@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,2090 @@
|
|
|
1
|
+
import { test, before, beforeEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import type { PiExtensionUiRequest, SessionDeleteResult, SessionListItem, SessionRuntimeInfo, SessionState, Workspace } from '../../shared/ipc-contracts'
|
|
4
|
+
import type { PreviewTarget } from './store'
|
|
5
|
+
|
|
6
|
+
// Each recorded call is appended to `calls`, so tests can assert both that a
|
|
7
|
+
// session change reached Pi and that nothing reached Pi when it was declined.
|
|
8
|
+
const calls: string[] = []
|
|
9
|
+
let switchResult: { success?: boolean; error?: string } | SessionRuntimeInfo | null = { success: true }
|
|
10
|
+
// Non-null makes the stubbed pi.getStatus reject, simulating a main-side
|
|
11
|
+
// failure AFTER a workspace switch has already committed.
|
|
12
|
+
let getStatusFailure: string | null = null
|
|
13
|
+
// Status the stubbed pi.getStatus reports for the ACTIVE workspace. A live
|
|
14
|
+
// background turn means the target workspace's own process is running.
|
|
15
|
+
let piStatusResult: 'stopped' | 'running' = 'stopped'
|
|
16
|
+
// Non-null makes the stubbed workspace.setActive reject, simulating a switch
|
|
17
|
+
// that never commits on the main side.
|
|
18
|
+
let setActiveFailure: string | null = null
|
|
19
|
+
// Non-null makes the stubbed ui.getPendingPrompts reject, simulating a boot
|
|
20
|
+
// recovery that cannot reach main.
|
|
21
|
+
let pendingPromptsFailure: string | null = null
|
|
22
|
+
let pendingPromptsSnapshot: Record<string, number> = {}
|
|
23
|
+
// Non-null makes the stubbed workspace.getActivity reject; the activity
|
|
24
|
+
// snapshot is cosmetic and must never block prompt recovery.
|
|
25
|
+
let workspaceActivityFailure: string | null = null
|
|
26
|
+
let workspaceActivitySnapshot: Record<string, { state: 'working' | 'needs-approval' | 'completed' | 'failed'; since: number }> = {}
|
|
27
|
+
let activeWorkspaceResult: Workspace | null = null
|
|
28
|
+
let workspaceListResult: Workspace[] = []
|
|
29
|
+
// Results the stubbed files.search returns; the hook runs while the "IPC" is
|
|
30
|
+
// in flight so tests can interleave state changes with the await.
|
|
31
|
+
let fileSearchResults: Array<{
|
|
32
|
+
name: string
|
|
33
|
+
path: string
|
|
34
|
+
relativePath: string
|
|
35
|
+
matchType: string
|
|
36
|
+
}> = []
|
|
37
|
+
let fileSearchHook: (() => void) | null = null
|
|
38
|
+
// What the stubbed session.getState returns as data (null = no state).
|
|
39
|
+
let sessionStateResult: SessionState | null = null
|
|
40
|
+
// What the stubbed session.delete reports. `replacementSessionPath` mirrors
|
|
41
|
+
// the runtime main promoted while closing the deleted session's tab.
|
|
42
|
+
let sessionDeleteResult: SessionDeleteResult = { ok: true, method: 'trash', replacementSessionPath: null }
|
|
43
|
+
|
|
44
|
+
// Only sessionFile is read by the code under test; the cast keeps this
|
|
45
|
+
// fixture from churning as SessionState grows fields.
|
|
46
|
+
function sessionStateWith(sessionFile: string): SessionState {
|
|
47
|
+
return { sessionFile } as unknown as SessionState
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const SESSION_PATH = '/tmp/session-b.jsonl'
|
|
51
|
+
// The sibling session main promotes when the active one is deleted or closed.
|
|
52
|
+
const REPLACEMENT_SESSION_PATH = '/tmp/session-sibling.jsonl'
|
|
53
|
+
const FORK_ENTRY_ID = 'entry-7'
|
|
54
|
+
|
|
55
|
+
const WORKSPACE_ID = 'ws-2'
|
|
56
|
+
|
|
57
|
+
const WORKSPACE_ONE: Workspace = {
|
|
58
|
+
id: 'ws-1',
|
|
59
|
+
name: 'one',
|
|
60
|
+
path: '/tmp/one',
|
|
61
|
+
createdAt: 0,
|
|
62
|
+
lastActiveAt: 0,
|
|
63
|
+
color: '#000',
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const WORKSPACE_TWO: Workspace = {
|
|
67
|
+
id: WORKSPACE_ID,
|
|
68
|
+
name: 'two',
|
|
69
|
+
path: '/tmp/two',
|
|
70
|
+
createdAt: 0,
|
|
71
|
+
lastActiveAt: 0,
|
|
72
|
+
color: '#000',
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const EXTENSION_DIALOG: PiExtensionUiRequest = {
|
|
76
|
+
type: 'extension_ui_request',
|
|
77
|
+
id: 'req-dialog',
|
|
78
|
+
method: 'confirm',
|
|
79
|
+
title: 'Allow write?',
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const EXTENSION_NOTIFY: PiExtensionUiRequest = {
|
|
83
|
+
type: 'extension_ui_request',
|
|
84
|
+
id: 'req-notify',
|
|
85
|
+
method: 'notify',
|
|
86
|
+
message: 'build finished',
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const piDesktopStub = {
|
|
90
|
+
workspace: {
|
|
91
|
+
setActive: async (id: string) => {
|
|
92
|
+
if (setActiveFailure) throw new Error(setActiveFailure)
|
|
93
|
+
calls.push(`setActiveWorkspace:${id}`)
|
|
94
|
+
const hit = workspaceListResult.find((w) => w.id === id)
|
|
95
|
+
if (hit) activeWorkspaceResult = hit
|
|
96
|
+
return hit ?? { id, name: 'other', path: '/tmp/other', createdAt: 0, lastActiveAt: 0, color: '#000' }
|
|
97
|
+
},
|
|
98
|
+
list: async () => workspaceListResult,
|
|
99
|
+
getActive: async () => activeWorkspaceResult,
|
|
100
|
+
getActivity: async () => {
|
|
101
|
+
if (workspaceActivityFailure) throw new Error(workspaceActivityFailure)
|
|
102
|
+
return workspaceActivitySnapshot
|
|
103
|
+
},
|
|
104
|
+
create: async (name: string, path: string) => {
|
|
105
|
+
calls.push(`createWorkspace:${name}:${path}`)
|
|
106
|
+
// Mirror main: existing path activates that workspace.
|
|
107
|
+
const existing = workspaceListResult.find((w) => w.path === path)
|
|
108
|
+
if (existing) {
|
|
109
|
+
activeWorkspaceResult = existing
|
|
110
|
+
return existing
|
|
111
|
+
}
|
|
112
|
+
// Tests that pre-set getActive for create→activate without a prior list.
|
|
113
|
+
if (activeWorkspaceResult && activeWorkspaceResult.path === path) {
|
|
114
|
+
return activeWorkspaceResult
|
|
115
|
+
}
|
|
116
|
+
const created = {
|
|
117
|
+
id: `ws-new-${name}`,
|
|
118
|
+
name,
|
|
119
|
+
path,
|
|
120
|
+
createdAt: 0,
|
|
121
|
+
lastActiveAt: 0,
|
|
122
|
+
color: '#000',
|
|
123
|
+
}
|
|
124
|
+
workspaceListResult = [...workspaceListResult, created]
|
|
125
|
+
// First workspace becomes active; otherwise leave active alone.
|
|
126
|
+
if (!activeWorkspaceResult) activeWorkspaceResult = created
|
|
127
|
+
return created
|
|
128
|
+
},
|
|
129
|
+
remove: async (id: string) => {
|
|
130
|
+
calls.push(`removeWorkspace:${id}`)
|
|
131
|
+
},
|
|
132
|
+
changePath: async (id: string, path: string) => {
|
|
133
|
+
calls.push(`changePath:${id}:${path}`)
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
system: {
|
|
137
|
+
pathKind: async (filePath: string) => {
|
|
138
|
+
calls.push(`pathKind:${filePath}`)
|
|
139
|
+
return { exists: true, isDirectory: true }
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
files: {
|
|
143
|
+
search: async (query: string) => {
|
|
144
|
+
calls.push(`filesSearch:${query}`)
|
|
145
|
+
fileSearchHook?.()
|
|
146
|
+
return fileSearchResults
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
pi: {
|
|
150
|
+
getStatus: async () => {
|
|
151
|
+
if (getStatusFailure) throw new Error(getStatusFailure)
|
|
152
|
+
return { status: piStatusResult, pid: piStatusResult === 'running' ? 1 : null, error: null }
|
|
153
|
+
},
|
|
154
|
+
start: async () => {
|
|
155
|
+
calls.push('pi.start')
|
|
156
|
+
return { status: 'running' as const, pid: 1, error: null }
|
|
157
|
+
},
|
|
158
|
+
restart: async () => {
|
|
159
|
+
calls.push('pi.restart')
|
|
160
|
+
return { status: 'running' as const, pid: 1, error: null }
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
ui: {
|
|
164
|
+
respondSelect: (id: string, _value: string) => {
|
|
165
|
+
calls.push(`respondSelect:${id}`)
|
|
166
|
+
},
|
|
167
|
+
respondConfirm: (id: string, _confirmed: boolean) => {
|
|
168
|
+
calls.push(`respondConfirm:${id}`)
|
|
169
|
+
},
|
|
170
|
+
respondInput: (id: string, _value: string) => {
|
|
171
|
+
calls.push(`respondInput:${id}`)
|
|
172
|
+
},
|
|
173
|
+
respondEditor: (id: string, _value: string) => {
|
|
174
|
+
calls.push(`respondEditor:${id}`)
|
|
175
|
+
},
|
|
176
|
+
flushPendingPrompts: async (workspaceId: string) => {
|
|
177
|
+
calls.push(`flushPendingPrompts:${workspaceId}`)
|
|
178
|
+
},
|
|
179
|
+
getPendingPrompts: async () => {
|
|
180
|
+
if (pendingPromptsFailure) throw new Error(pendingPromptsFailure)
|
|
181
|
+
return pendingPromptsSnapshot
|
|
182
|
+
},
|
|
183
|
+
setEditorDirty: (dirty: boolean, fileName: string | null) => {
|
|
184
|
+
calls.push(`editorDirtyMirror:${dirty}:${fileName ?? ''}`)
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
commands: {
|
|
188
|
+
abort: async () => {
|
|
189
|
+
calls.push('abort')
|
|
190
|
+
return null
|
|
191
|
+
},
|
|
192
|
+
prompt: async (message: string) => {
|
|
193
|
+
calls.push(`prompt:${message}`)
|
|
194
|
+
return null
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
session: {
|
|
198
|
+
switch: async (path: string) => {
|
|
199
|
+
calls.push(`switch:${path}`)
|
|
200
|
+
return switchResult
|
|
201
|
+
},
|
|
202
|
+
createNew: async () => {
|
|
203
|
+
calls.push('createNew')
|
|
204
|
+
return { success: true }
|
|
205
|
+
},
|
|
206
|
+
closeRuntime: async (runtimeId: string) => {
|
|
207
|
+
calls.push(`closeRuntime:${runtimeId}`)
|
|
208
|
+
return { runtimeId, workspaceId: WORKSPACE_ONE.id, sessionPath: SESSION_PATH, empty: false, deleted: false }
|
|
209
|
+
},
|
|
210
|
+
delete: async (path: string) => {
|
|
211
|
+
calls.push(`deleteSession:${path}`)
|
|
212
|
+
return sessionDeleteResult
|
|
213
|
+
},
|
|
214
|
+
fork: async (entryId: string) => {
|
|
215
|
+
calls.push(`fork:${entryId}`)
|
|
216
|
+
return { success: true }
|
|
217
|
+
},
|
|
218
|
+
clone: async () => {
|
|
219
|
+
calls.push('clone')
|
|
220
|
+
return { success: true }
|
|
221
|
+
},
|
|
222
|
+
getMessages: async () => {
|
|
223
|
+
calls.push('getMessages')
|
|
224
|
+
return { success: true, data: { messages: [] } }
|
|
225
|
+
},
|
|
226
|
+
getState: async () => ({ success: true, data: sessionStateResult }),
|
|
227
|
+
getStats: async () => ({ success: true, data: null }),
|
|
228
|
+
list: async () => [],
|
|
229
|
+
},
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
type AppStore = typeof import('./store')['useAppStore']
|
|
233
|
+
let useAppStore: AppStore
|
|
234
|
+
let countPromptsWaitingElsewhere: typeof import('./store')['countPromptsWaitingElsewhere']
|
|
235
|
+
let formatPromptsWaiting: typeof import('./store')['formatPromptsWaiting']
|
|
236
|
+
let openFileFromChat: typeof import('./components/chat-file-link')['openFileFromChat']
|
|
237
|
+
let DIALOG_OVERLAY_Z_INDEX: number
|
|
238
|
+
let NOTIFY_TOAST_Z_INDEX: number
|
|
239
|
+
|
|
240
|
+
// The store reaches for `window.piDesktop` inside its actions, so the bridge has
|
|
241
|
+
// to exist before the module body runs — hence the deferred import.
|
|
242
|
+
before(async () => {
|
|
243
|
+
;(globalThis as unknown as { window: unknown }).window = { piDesktop: piDesktopStub }
|
|
244
|
+
;({ useAppStore, countPromptsWaitingElsewhere, formatPromptsWaiting } = await import('./store'))
|
|
245
|
+
;({ openFileFromChat } = await import('./components/chat-file-link'))
|
|
246
|
+
;({ DIALOG_OVERLAY_Z_INDEX, NOTIFY_TOAST_Z_INDEX } = await import('./components/extension-ui-dialog'))
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
// A turn in flight: streaming flag set, partial buffers filled, and a queue
|
|
250
|
+
// update already applied — exactly the state a session change has to tear down.
|
|
251
|
+
function enterStreamingState(): void {
|
|
252
|
+
useAppStore.setState({
|
|
253
|
+
isStreaming: true,
|
|
254
|
+
streamingContent: 'partial answer',
|
|
255
|
+
streamingThinking: 'partial thinking',
|
|
256
|
+
streamingToolCalls: new Map([
|
|
257
|
+
['call-1', { name: 'read', args: '{}', isExecuting: true }],
|
|
258
|
+
]),
|
|
259
|
+
pendingSteering: ['steer me'],
|
|
260
|
+
pendingFollowUp: ['follow up'],
|
|
261
|
+
messages: [{ id: 'm1', role: 'user', content: 'hello', timestamp: 0 }],
|
|
262
|
+
promptHistory: ['hello'],
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Answers confirmation dialogs the store raises, in order. Polling keeps the
|
|
267
|
+
// action under test awaiting a real promise, as it does against the rendered
|
|
268
|
+
// dialog; a single poller answers each dialog as it appears. Arming cancels
|
|
269
|
+
// any previous poller — a stray from a test whose dialogs never appeared must
|
|
270
|
+
// not answer a later test's dialog with the wrong value.
|
|
271
|
+
let answerPoll: ReturnType<typeof setInterval> | null = null
|
|
272
|
+
|
|
273
|
+
function answerConfirms(values: boolean[]): void {
|
|
274
|
+
if (answerPoll !== null) clearInterval(answerPoll)
|
|
275
|
+
let next = 0
|
|
276
|
+
const poll = setInterval(() => {
|
|
277
|
+
if (next >= values.length) {
|
|
278
|
+
clearInterval(poll)
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
if (useAppStore.getState().confirmRequest) {
|
|
282
|
+
useAppStore.getState().resolveConfirm(values[next])
|
|
283
|
+
next++
|
|
284
|
+
}
|
|
285
|
+
}, 0)
|
|
286
|
+
answerPoll = poll
|
|
287
|
+
// Never leave the timer running if the dialogs are not raised at all.
|
|
288
|
+
setTimeout(() => clearInterval(poll), 100)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function answerConfirm(confirmed: boolean): void {
|
|
292
|
+
answerConfirms([confirmed])
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
beforeEach(() => {
|
|
296
|
+
if (answerPoll !== null) {
|
|
297
|
+
clearInterval(answerPoll)
|
|
298
|
+
answerPoll = null
|
|
299
|
+
}
|
|
300
|
+
switchResult = { success: true }
|
|
301
|
+
getStatusFailure = null
|
|
302
|
+
piStatusResult = 'stopped'
|
|
303
|
+
setActiveFailure = null
|
|
304
|
+
pendingPromptsSnapshot = {}
|
|
305
|
+
workspaceActivityFailure = null
|
|
306
|
+
workspaceActivitySnapshot = {}
|
|
307
|
+
activeWorkspaceResult = null
|
|
308
|
+
workspaceListResult = []
|
|
309
|
+
fileSearchResults = []
|
|
310
|
+
fileSearchHook = null
|
|
311
|
+
sessionStateResult = null
|
|
312
|
+
sessionDeleteResult = { ok: true, method: 'trash', replacementSessionPath: null }
|
|
313
|
+
useAppStore.setState({
|
|
314
|
+
isStreaming: false,
|
|
315
|
+
streamingContent: '',
|
|
316
|
+
streamingThinking: '',
|
|
317
|
+
streamingToolCalls: new Map(),
|
|
318
|
+
pendingSteering: [],
|
|
319
|
+
pendingFollowUp: [],
|
|
320
|
+
messages: [],
|
|
321
|
+
promptHistory: [],
|
|
322
|
+
sessionState: null,
|
|
323
|
+
confirmRequest: null,
|
|
324
|
+
extensionUiRequest: null,
|
|
325
|
+
extensionNotify: null,
|
|
326
|
+
pendingPromptCounts: {},
|
|
327
|
+
workspaceActivity: {},
|
|
328
|
+
reattachedMidTurn: false,
|
|
329
|
+
sessionLoading: false,
|
|
330
|
+
sessionRuntimes: {},
|
|
331
|
+
activeSessionRuntimeId: null,
|
|
332
|
+
activeWorkspace: null,
|
|
333
|
+
workspaces: [],
|
|
334
|
+
piStatus: 'stopped',
|
|
335
|
+
currentView: 'home',
|
|
336
|
+
previewTarget: null,
|
|
337
|
+
chatSidePanel: null,
|
|
338
|
+
editorDirty: false,
|
|
339
|
+
})
|
|
340
|
+
// AFTER the state reset: the editor-dirty mirror subscription fires on the
|
|
341
|
+
// reset itself when the previous test left the flag set, and that push
|
|
342
|
+
// belongs to cleanup, not to the test about to run.
|
|
343
|
+
calls.length = 0
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
test('a ready new runtime hydrates while the empty session is still loading', async () => {
|
|
347
|
+
useAppStore.setState({
|
|
348
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
349
|
+
workspaces: [WORKSPACE_ONE],
|
|
350
|
+
activeSessionRuntimeId: 'rt-new',
|
|
351
|
+
sessionLoading: true,
|
|
352
|
+
sessionState: null,
|
|
353
|
+
messages: [],
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
const runtime: SessionRuntimeInfo = {
|
|
357
|
+
runtimeId: 'rt-new',
|
|
358
|
+
workspaceId: WORKSPACE_ONE.id,
|
|
359
|
+
sessionPath: SESSION_PATH,
|
|
360
|
+
sessionId: 'session-new',
|
|
361
|
+
status: 'running',
|
|
362
|
+
pid: 123,
|
|
363
|
+
error: null,
|
|
364
|
+
activity: null,
|
|
365
|
+
active: true,
|
|
366
|
+
}
|
|
367
|
+
useAppStore.getState().handleSessionRuntime(runtime)
|
|
368
|
+
await new Promise<void>((resolve) => setImmediate(resolve))
|
|
369
|
+
|
|
370
|
+
assert.equal(calls.includes('getMessages'), true)
|
|
371
|
+
assert.equal(useAppStore.getState().sessionLoading, false)
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
test('an active runtime error ends the session loading state', () => {
|
|
375
|
+
useAppStore.setState({
|
|
376
|
+
activeSessionRuntimeId: 'rt-failed',
|
|
377
|
+
sessionLoading: true,
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
useAppStore.getState().handleSessionRuntime({
|
|
381
|
+
runtimeId: 'rt-failed',
|
|
382
|
+
workspaceId: WORKSPACE_ONE.id,
|
|
383
|
+
sessionPath: null,
|
|
384
|
+
sessionId: null,
|
|
385
|
+
status: 'error',
|
|
386
|
+
pid: null,
|
|
387
|
+
error: 'Pi failed to start',
|
|
388
|
+
activity: 'failed',
|
|
389
|
+
active: true,
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
assert.equal(useAppStore.getState().sessionLoading, false)
|
|
393
|
+
assert.equal(useAppStore.getState().piError, 'Pi failed to start')
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
test('closed runtime events remove session tabs from renderer state', () => {
|
|
397
|
+
useAppStore.setState({
|
|
398
|
+
sessionRuntimes: {
|
|
399
|
+
'rt-closed': {
|
|
400
|
+
runtimeId: 'rt-closed',
|
|
401
|
+
workspaceId: WORKSPACE_ONE.id,
|
|
402
|
+
sessionPath: SESSION_PATH,
|
|
403
|
+
sessionId: 'closed-session',
|
|
404
|
+
status: 'stopped',
|
|
405
|
+
pid: null,
|
|
406
|
+
error: null,
|
|
407
|
+
activity: null,
|
|
408
|
+
active: true,
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
activeSessionRuntimeId: 'rt-closed',
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
useAppStore.getState().handleSessionRuntime({
|
|
415
|
+
runtimeId: 'rt-closed',
|
|
416
|
+
workspaceId: WORKSPACE_ONE.id,
|
|
417
|
+
sessionPath: SESSION_PATH,
|
|
418
|
+
sessionId: 'closed-session',
|
|
419
|
+
status: 'stopped',
|
|
420
|
+
pid: null,
|
|
421
|
+
error: null,
|
|
422
|
+
activity: null,
|
|
423
|
+
active: false,
|
|
424
|
+
closed: true,
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
assert.equal(useAppStore.getState().sessionRuntimes['rt-closed'], undefined)
|
|
428
|
+
assert.equal(useAppStore.getState().activeSessionRuntimeId, null)
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
test('clearMessages resets every per-turn streaming field', () => {
|
|
432
|
+
enterStreamingState()
|
|
433
|
+
|
|
434
|
+
useAppStore.getState().clearMessages()
|
|
435
|
+
|
|
436
|
+
const state = useAppStore.getState()
|
|
437
|
+
assert.equal(state.isStreaming, false, 'isStreaming must not survive a chat reset')
|
|
438
|
+
assert.equal(state.streamingContent, '')
|
|
439
|
+
assert.equal(state.streamingThinking, '')
|
|
440
|
+
assert.equal(state.streamingToolCalls.size, 0)
|
|
441
|
+
assert.deepEqual(state.pendingSteering, [])
|
|
442
|
+
assert.deepEqual(state.pendingFollowUp, [])
|
|
443
|
+
assert.deepEqual(state.messages, [])
|
|
444
|
+
assert.deepEqual(state.promptHistory, [])
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
test('confirmSessionChange passes straight through when no turn is streaming', async () => {
|
|
448
|
+
const proceed = await useAppStore.getState().confirmSessionChange('switch')
|
|
449
|
+
|
|
450
|
+
assert.equal(proceed, true)
|
|
451
|
+
assert.equal(useAppStore.getState().confirmRequest, null, 'an idle Pi must not raise a dialog')
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
test('confirmSessionChange labels the dialog for the action being confirmed', async () => {
|
|
455
|
+
enterStreamingState()
|
|
456
|
+
|
|
457
|
+
const pending = useAppStore.getState().confirmSessionChange('fork')
|
|
458
|
+
const request = useAppStore.getState().confirmRequest
|
|
459
|
+
assert.ok(request, 'a streaming turn must raise the dialog')
|
|
460
|
+
assert.equal(request.confirmLabel, 'Fork anyway')
|
|
461
|
+
assert.equal(request.cancelLabel, 'Keep working')
|
|
462
|
+
assert.match(request.message, /Forking this session/)
|
|
463
|
+
assert.equal(request.danger, true, 'discarding a running turn must not be the default action')
|
|
464
|
+
|
|
465
|
+
useAppStore.getState().resolveConfirm(false)
|
|
466
|
+
assert.equal(await pending, false)
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
test('switching sessions does not warn or stop the previous process', async () => {
|
|
470
|
+
enterStreamingState()
|
|
471
|
+
|
|
472
|
+
await useAppStore.getState().switchSession(SESSION_PATH)
|
|
473
|
+
|
|
474
|
+
const state = useAppStore.getState()
|
|
475
|
+
assert.equal(calls.includes(`switch:${SESSION_PATH}`), true)
|
|
476
|
+
assert.equal(state.isStreaming, false, 'the newly selected session starts with a clean local stream')
|
|
477
|
+
assert.equal(state.streamingContent, '')
|
|
478
|
+
assert.deepEqual(state.pendingSteering, [])
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
test('switching sessions clears the local streaming state', async () => {
|
|
482
|
+
enterStreamingState()
|
|
483
|
+
await useAppStore.getState().switchSession(SESSION_PATH)
|
|
484
|
+
|
|
485
|
+
const state = useAppStore.getState()
|
|
486
|
+
assert.equal(calls[0], `switch:${SESSION_PATH}`)
|
|
487
|
+
assert.equal(state.isStreaming, false, 'the abandoned turn must not leave a stuck spinner')
|
|
488
|
+
assert.equal(state.streamingContent, '')
|
|
489
|
+
assert.deepEqual(state.pendingSteering, [], 'the old queue counters must not carry over')
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
test('switchSession does not warn when Pi is idle', async () => {
|
|
493
|
+
await useAppStore.getState().switchSession(SESSION_PATH)
|
|
494
|
+
|
|
495
|
+
assert.equal(useAppStore.getState().confirmRequest, null)
|
|
496
|
+
assert.equal(calls[0], `switch:${SESSION_PATH}`)
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
test('switchSession clears streaming state even when Pi refuses the switch', async () => {
|
|
500
|
+
enterStreamingState()
|
|
501
|
+
answerConfirm(true)
|
|
502
|
+
switchResult = { success: false, error: 'Pi not running. Start Pi first.' }
|
|
503
|
+
|
|
504
|
+
await useAppStore.getState().switchSession(SESSION_PATH)
|
|
505
|
+
|
|
506
|
+
const state = useAppStore.getState()
|
|
507
|
+
assert.equal(state.isStreaming, false, 'a refused switch must still leave the composer usable')
|
|
508
|
+
assert.equal(calls.includes('getMessages'), false, 'a refused switch must not reload history')
|
|
509
|
+
assert.equal(
|
|
510
|
+
state.messages.some((m) => m.role === 'system' && m.content.includes('Pi not running')),
|
|
511
|
+
true,
|
|
512
|
+
'the refusal reason must be shown to the user'
|
|
513
|
+
)
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
test('createNewSession starts an independent runtime without warning', async () => {
|
|
517
|
+
enterStreamingState()
|
|
518
|
+
|
|
519
|
+
await useAppStore.getState().createNewSession()
|
|
520
|
+
|
|
521
|
+
assert.equal(calls.includes('createNew'), true)
|
|
522
|
+
assert.equal(useAppStore.getState().isStreaming, false)
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
test('createNewSession opens the new conversation in Chat', async () => {
|
|
526
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
527
|
+
workspaceListResult = [WORKSPACE_ONE]
|
|
528
|
+
useAppStore.setState({
|
|
529
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
530
|
+
workspaces: [WORKSPACE_ONE],
|
|
531
|
+
currentView: 'sessions',
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
await useAppStore.getState().createNewSession()
|
|
535
|
+
|
|
536
|
+
const state = useAppStore.getState()
|
|
537
|
+
assert.equal(calls.includes('createNew'), true)
|
|
538
|
+
assert.equal(state.currentView, 'chat')
|
|
539
|
+
assert.equal(state.sessionLoading, false, 'an empty new session should render immediately')
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
test('forkFrom is gated by the same warning', async () => {
|
|
543
|
+
enterStreamingState()
|
|
544
|
+
answerConfirm(false)
|
|
545
|
+
|
|
546
|
+
await useAppStore.getState().forkFrom(FORK_ENTRY_ID)
|
|
547
|
+
|
|
548
|
+
assert.deepEqual(calls, [], 'a declined fork must not reach Pi')
|
|
549
|
+
assert.equal(useAppStore.getState().isStreaming, true)
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
// Workspace switches are safe because each workspace owns a separate Pi
|
|
553
|
+
// process. Switching tabs must not block on, abort, or warn about the turn that
|
|
554
|
+
// remains active in the background.
|
|
555
|
+
test('switchWorkspace leaves a running Pi in the background without warning', async () => {
|
|
556
|
+
enterStreamingState()
|
|
557
|
+
|
|
558
|
+
const proceed = await useAppStore.getState().switchWorkspace(WORKSPACE_ID)
|
|
559
|
+
|
|
560
|
+
assert.equal(proceed, true)
|
|
561
|
+
assert.equal(calls[0], `setActiveWorkspace:${WORKSPACE_ID}`)
|
|
562
|
+
assert.equal(useAppStore.getState().isStreaming, false)
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
test('switchWorkspace does not warn when Pi is idle', async () => {
|
|
566
|
+
const proceed = await useAppStore.getState().switchWorkspace(WORKSPACE_ID)
|
|
567
|
+
|
|
568
|
+
assert.equal(proceed, true)
|
|
569
|
+
assert.equal(useAppStore.getState().confirmRequest, null)
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
test('cloneBranch is gated by the same warning', async () => {
|
|
573
|
+
enterStreamingState()
|
|
574
|
+
answerConfirm(false)
|
|
575
|
+
|
|
576
|
+
await useAppStore.getState().cloneBranch()
|
|
577
|
+
|
|
578
|
+
assert.deepEqual(calls, [], 'a declined clone must not reach Pi')
|
|
579
|
+
assert.equal(useAppStore.getState().isStreaming, true)
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
// ─── Cross-workspace extension-UI prompts (queue-and-replay) ─────────────────
|
|
583
|
+
|
|
584
|
+
test('an accepted workspace switch clears the held dialog without answering it', async () => {
|
|
585
|
+
useAppStore.setState({ extensionUiRequest: EXTENSION_DIALOG })
|
|
586
|
+
|
|
587
|
+
const proceed = await useAppStore.getState().switchWorkspace(WORKSPACE_ID)
|
|
588
|
+
|
|
589
|
+
assert.equal(proceed, true)
|
|
590
|
+
assert.equal(useAppStore.getState().extensionUiRequest, null, 'the old workspace dialog must leave the screen')
|
|
591
|
+
assert.equal(
|
|
592
|
+
calls.some((c) => c.startsWith('respond')),
|
|
593
|
+
false,
|
|
594
|
+
'clearing the slot must not synthesize an answer — a false deny hard-blocks the asking tool'
|
|
595
|
+
)
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
test('a workspace switch clears the old dialog without answering it', async () => {
|
|
599
|
+
useAppStore.setState({ extensionUiRequest: EXTENSION_DIALOG })
|
|
600
|
+
|
|
601
|
+
const proceed = await useAppStore.getState().switchWorkspace(WORKSPACE_ID)
|
|
602
|
+
|
|
603
|
+
assert.equal(proceed, true)
|
|
604
|
+
assert.equal(useAppStore.getState().extensionUiRequest, null)
|
|
605
|
+
assert.equal(calls.includes(`flushPendingPrompts:${WORKSPACE_ID}`), true)
|
|
606
|
+
assert.equal(
|
|
607
|
+
calls.some((c) => c.startsWith('respond')),
|
|
608
|
+
false,
|
|
609
|
+
'switching tabs must not synthesize a deny for the background prompt'
|
|
610
|
+
)
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
test('a successful workspace switch flushes prompts for the new workspace', async () => {
|
|
614
|
+
const proceed = await useAppStore.getState().switchWorkspace(WORKSPACE_ID)
|
|
615
|
+
|
|
616
|
+
assert.equal(proceed, true)
|
|
617
|
+
assert.equal(calls.includes(`flushPendingPrompts:${WORKSPACE_ID}`), true)
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
// Design invariant: the dialog slot may only be cleared once setActive has
|
|
621
|
+
// committed. Clearing it earlier loses the prompt from the screen of a
|
|
622
|
+
// workspace the user never actually left.
|
|
623
|
+
test('a failed setActive keeps the dialog on screen and replays nothing', async () => {
|
|
624
|
+
setActiveFailure = 'workspace backend gone'
|
|
625
|
+
useAppStore.setState({ extensionUiRequest: EXTENSION_DIALOG })
|
|
626
|
+
|
|
627
|
+
const proceed = await useAppStore.getState().switchWorkspace(WORKSPACE_ID)
|
|
628
|
+
|
|
629
|
+
assert.equal(proceed, false, 'a switch that never committed must report failure')
|
|
630
|
+
assert.equal(
|
|
631
|
+
useAppStore.getState().extensionUiRequest?.id,
|
|
632
|
+
EXTENSION_DIALOG.id,
|
|
633
|
+
'the dialog still belongs to the workspace on screen'
|
|
634
|
+
)
|
|
635
|
+
assert.equal(
|
|
636
|
+
calls.some((c) => c.startsWith('flushPendingPrompts')),
|
|
637
|
+
false,
|
|
638
|
+
'nothing may be replayed for a workspace that never became active'
|
|
639
|
+
)
|
|
640
|
+
})
|
|
641
|
+
|
|
642
|
+
test('the flush still runs when a step after the committed switch rejects', async () => {
|
|
643
|
+
getStatusFailure = 'status backend gone'
|
|
644
|
+
|
|
645
|
+
const proceed = await useAppStore.getState().switchWorkspace(WORKSPACE_ID)
|
|
646
|
+
|
|
647
|
+
assert.equal(proceed, false, 'the caller must learn the chain failed')
|
|
648
|
+
assert.equal(calls.includes(`setActiveWorkspace:${WORKSPACE_ID}`), true)
|
|
649
|
+
assert.equal(
|
|
650
|
+
calls.includes(`flushPendingPrompts:${WORKSPACE_ID}`),
|
|
651
|
+
true,
|
|
652
|
+
'the switch committed on the main side, so the held prompt must still be replayed'
|
|
653
|
+
)
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
test('notify and dialog requests occupy separate slots in either order', () => {
|
|
657
|
+
useAppStore.getState().handlePiEvent(EXTENSION_DIALOG)
|
|
658
|
+
useAppStore.getState().handlePiEvent(EXTENSION_NOTIFY)
|
|
659
|
+
|
|
660
|
+
let state = useAppStore.getState()
|
|
661
|
+
assert.equal(state.extensionUiRequest?.id, EXTENSION_DIALOG.id, 'a toast must never clobber a blocking dialog')
|
|
662
|
+
assert.equal(state.extensionNotify?.id, EXTENSION_NOTIFY.id)
|
|
663
|
+
|
|
664
|
+
useAppStore.setState({ extensionUiRequest: null, extensionNotify: null })
|
|
665
|
+
useAppStore.getState().handlePiEvent(EXTENSION_NOTIFY)
|
|
666
|
+
useAppStore.getState().handlePiEvent(EXTENSION_DIALOG)
|
|
667
|
+
|
|
668
|
+
state = useAppStore.getState()
|
|
669
|
+
assert.equal(state.extensionNotify?.id, EXTENSION_NOTIFY.id, 'a dialog must never clobber a toast')
|
|
670
|
+
assert.equal(state.extensionUiRequest?.id, EXTENSION_DIALOG.id)
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
test('dismissing a notify toast answers it and leaves the dialog slot alone', () => {
|
|
674
|
+
useAppStore.setState({ extensionUiRequest: EXTENSION_DIALOG, extensionNotify: EXTENSION_NOTIFY })
|
|
675
|
+
|
|
676
|
+
useAppStore.getState().dismissExtensionNotify()
|
|
677
|
+
|
|
678
|
+
const state = useAppStore.getState()
|
|
679
|
+
assert.equal(state.extensionNotify, null)
|
|
680
|
+
assert.equal(state.extensionUiRequest?.id, EXTENSION_DIALOG.id)
|
|
681
|
+
assert.deepEqual(
|
|
682
|
+
calls,
|
|
683
|
+
[`respondInput:${EXTENSION_NOTIFY.id}`],
|
|
684
|
+
'toast dismissal keeps sending the empty-input response Pi ignores'
|
|
685
|
+
)
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
test('pending prompt counts land in state and sum over non-active workspaces', () => {
|
|
689
|
+
useAppStore.getState().handlePendingPromptCounts({ 'ws-2': 2, 'ws-9': 1 })
|
|
690
|
+
|
|
691
|
+
assert.deepEqual(useAppStore.getState().pendingPromptCounts, { 'ws-2': 2, 'ws-9': 1 })
|
|
692
|
+
assert.equal(countPromptsWaitingElsewhere({ 'ws-2': 2, 'ws-9': 1 }, 'ws-2'), 1)
|
|
693
|
+
assert.equal(countPromptsWaitingElsewhere({ 'ws-2': 2, 'ws-9': 1 }, null), 3)
|
|
694
|
+
assert.equal(formatPromptsWaiting(1), '1 Pi prompt waiting')
|
|
695
|
+
assert.equal(formatPromptsWaiting(3), '3 Pi prompts waiting')
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
test('removing the workspace flushes prompts only when a new one is promoted', async () => {
|
|
699
|
+
useAppStore.setState({ activeWorkspace: WORKSPACE_ONE, extensionUiRequest: EXTENSION_DIALOG })
|
|
700
|
+
activeWorkspaceResult = WORKSPACE_TWO
|
|
701
|
+
answerConfirm(true)
|
|
702
|
+
|
|
703
|
+
await useAppStore.getState().removeWorkspace(WORKSPACE_ONE.id)
|
|
704
|
+
|
|
705
|
+
assert.equal(
|
|
706
|
+
calls.includes(`flushPendingPrompts:${WORKSPACE_ID}`),
|
|
707
|
+
true,
|
|
708
|
+
'the promoted workspace may hold a prompt that must surface now'
|
|
709
|
+
)
|
|
710
|
+
assert.equal(
|
|
711
|
+
useAppStore.getState().extensionUiRequest,
|
|
712
|
+
null,
|
|
713
|
+
'the dialog belonged to the workspace that is gone'
|
|
714
|
+
)
|
|
715
|
+
assert.equal(
|
|
716
|
+
calls.some((c) => c.startsWith('respond')),
|
|
717
|
+
false,
|
|
718
|
+
'clearing the slot must not synthesize an answer'
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
calls.length = 0
|
|
722
|
+
useAppStore.setState({ activeWorkspace: activeWorkspaceResult, extensionUiRequest: EXTENSION_DIALOG })
|
|
723
|
+
answerConfirm(true)
|
|
724
|
+
await useAppStore.getState().removeWorkspace('ws-9')
|
|
725
|
+
|
|
726
|
+
assert.equal(
|
|
727
|
+
calls.some((c) => c.startsWith('flushPendingPrompts')),
|
|
728
|
+
false,
|
|
729
|
+
'removing a non-active workspace changes nothing on screen'
|
|
730
|
+
)
|
|
731
|
+
assert.equal(
|
|
732
|
+
useAppStore.getState().extensionUiRequest?.id,
|
|
733
|
+
EXTENSION_DIALOG.id,
|
|
734
|
+
'the active workspace keeps its unanswered dialog'
|
|
735
|
+
)
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
// Regression: main activates the existing workspace when a create names a path
|
|
739
|
+
// it already knows (and when it creates the very first workspace). The renderer
|
|
740
|
+
// never routed that through switchWorkspace, so without adopting the change the
|
|
741
|
+
// newly-active workspace's held prompt stays invisible — the badge hides it
|
|
742
|
+
// (it counts other workspaces only) and no dialog is ever broadcast.
|
|
743
|
+
test('a create that main turns into an activation adopts the new workspace', async () => {
|
|
744
|
+
useAppStore.setState({ activeWorkspace: WORKSPACE_ONE, extensionUiRequest: EXTENSION_DIALOG })
|
|
745
|
+
activeWorkspaceResult = WORKSPACE_TWO
|
|
746
|
+
|
|
747
|
+
await useAppStore.getState().createWorkspace(WORKSPACE_TWO.name, WORKSPACE_TWO.path)
|
|
748
|
+
|
|
749
|
+
assert.equal(calls.includes(`createWorkspace:${WORKSPACE_TWO.name}:${WORKSPACE_TWO.path}`), true)
|
|
750
|
+
assert.equal(
|
|
751
|
+
useAppStore.getState().extensionUiRequest,
|
|
752
|
+
null,
|
|
753
|
+
'the dialog belongs to the workspace that just left the screen'
|
|
754
|
+
)
|
|
755
|
+
assert.equal(
|
|
756
|
+
calls.some((c) => c.startsWith('respond')),
|
|
757
|
+
false,
|
|
758
|
+
'clearing the slot must not synthesize an answer — a false deny hard-blocks the asking tool'
|
|
759
|
+
)
|
|
760
|
+
assert.equal(
|
|
761
|
+
calls.includes(`flushPendingPrompts:${WORKSPACE_TWO.id}`),
|
|
762
|
+
true,
|
|
763
|
+
'a prompt held for the workspace now on screen must be replayed'
|
|
764
|
+
)
|
|
765
|
+
})
|
|
766
|
+
|
|
767
|
+
test('a create that leaves the active workspace alone touches neither slot nor prompts', async () => {
|
|
768
|
+
useAppStore.setState({ activeWorkspace: WORKSPACE_ONE, extensionUiRequest: EXTENSION_DIALOG })
|
|
769
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
770
|
+
// New path while one is already active — main does not switch away.
|
|
771
|
+
workspaceListResult = [WORKSPACE_ONE]
|
|
772
|
+
|
|
773
|
+
await useAppStore.getState().createWorkspace(WORKSPACE_TWO.name, WORKSPACE_TWO.path)
|
|
774
|
+
|
|
775
|
+
assert.equal(useAppStore.getState().extensionUiRequest?.id, EXTENSION_DIALOG.id)
|
|
776
|
+
assert.equal(
|
|
777
|
+
calls.some((c) => c.startsWith('flushPendingPrompts')),
|
|
778
|
+
false,
|
|
779
|
+
'the workspace on screen did not change, so nothing needs replaying'
|
|
780
|
+
)
|
|
781
|
+
})
|
|
782
|
+
|
|
783
|
+
// Regression: openFolderAsWorkspace must not treat "main activated the target on
|
|
784
|
+
// create" as "we were already on that workspace". Skipping switchWorkspace leaves
|
|
785
|
+
// the previous chat/messages on screen.
|
|
786
|
+
test('openFolderAsWorkspace switches when the dropped folder is an existing other workspace', async () => {
|
|
787
|
+
workspaceListResult = [WORKSPACE_ONE, WORKSPACE_TWO]
|
|
788
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
789
|
+
useAppStore.setState({
|
|
790
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
791
|
+
workspaces: [WORKSPACE_ONE, WORKSPACE_TWO],
|
|
792
|
+
messages: [{ id: 'old', role: 'user', content: 'from workspace one', timestamp: 0 }],
|
|
793
|
+
piStatus: 'running',
|
|
794
|
+
currentView: 'home',
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
const ok = await useAppStore.getState().openFolderAsWorkspace(WORKSPACE_TWO.path)
|
|
798
|
+
|
|
799
|
+
assert.equal(ok, true)
|
|
800
|
+
assert.equal(
|
|
801
|
+
calls.includes(`setActiveWorkspace:${WORKSPACE_TWO.id}`),
|
|
802
|
+
true,
|
|
803
|
+
'must route through switchWorkspace so messages and Pi status resync'
|
|
804
|
+
)
|
|
805
|
+
assert.deepEqual(
|
|
806
|
+
useAppStore.getState().messages,
|
|
807
|
+
[],
|
|
808
|
+
'previous workspace chat must clear on switch'
|
|
809
|
+
)
|
|
810
|
+
assert.equal(useAppStore.getState().currentView, 'chat')
|
|
811
|
+
assert.equal(
|
|
812
|
+
useAppStore.getState().piStatus,
|
|
813
|
+
'stopped',
|
|
814
|
+
'an idle target shows the empty view instantly — no process is spawned'
|
|
815
|
+
)
|
|
816
|
+
assert.equal(
|
|
817
|
+
calls.includes('pi.start'),
|
|
818
|
+
false,
|
|
819
|
+
'navigation must not spawn a process'
|
|
820
|
+
)
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
test('activating an idle workspace shows the empty view without starting Pi', async () => {
|
|
824
|
+
workspaceListResult = [WORKSPACE_ONE, WORKSPACE_TWO]
|
|
825
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
826
|
+
useAppStore.setState({
|
|
827
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
828
|
+
workspaces: [WORKSPACE_ONE, WORKSPACE_TWO],
|
|
829
|
+
messages: [{ id: 'old', role: 'user', content: 'old chat', timestamp: 0 }],
|
|
830
|
+
piStatus: 'running',
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
const ok = await useAppStore.getState().switchWorkspace(WORKSPACE_TWO.id)
|
|
834
|
+
|
|
835
|
+
assert.equal(ok, true)
|
|
836
|
+
const state = useAppStore.getState()
|
|
837
|
+
assert.equal(calls.includes('pi.start'), false, 'selection must stay process-free')
|
|
838
|
+
assert.equal(state.activeWorkspace?.id, WORKSPACE_TWO.id)
|
|
839
|
+
assert.deepEqual(state.messages, [])
|
|
840
|
+
assert.equal(state.sessionLoading, false, 'no spinner without a process')
|
|
841
|
+
assert.equal(state.sessionState, null)
|
|
842
|
+
assert.equal(state.piStatus, 'stopped')
|
|
843
|
+
})
|
|
844
|
+
|
|
845
|
+
test('the first prompt lazy-starts an idle workspace', async () => {
|
|
846
|
+
workspaceListResult = [WORKSPACE_ONE, WORKSPACE_TWO]
|
|
847
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
848
|
+
useAppStore.setState({
|
|
849
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
850
|
+
workspaces: [WORKSPACE_ONE, WORKSPACE_TWO],
|
|
851
|
+
})
|
|
852
|
+
await useAppStore.getState().switchWorkspace(WORKSPACE_TWO.id)
|
|
853
|
+
calls.length = 0
|
|
854
|
+
|
|
855
|
+
await useAppStore.getState().sendPrompt('ship it')
|
|
856
|
+
|
|
857
|
+
const startAt = calls.indexOf('pi.start')
|
|
858
|
+
assert.notEqual(startAt, -1, 'the first send must spawn the agent')
|
|
859
|
+
assert.equal(
|
|
860
|
+
calls.findIndex((c) => c.startsWith('prompt:')),
|
|
861
|
+
startAt + 1,
|
|
862
|
+
'the prompt goes out only after startup completes'
|
|
863
|
+
)
|
|
864
|
+
assert.equal(useAppStore.getState().piStatus, 'running')
|
|
865
|
+
})
|
|
866
|
+
|
|
867
|
+
// Runtime snapshots as main pushes them. `active` marks the one runtime main
|
|
868
|
+
// resolves for the workspace — the only manager a prompt can reach.
|
|
869
|
+
function runtimeIn(workspace: Workspace, overrides: Partial<SessionRuntimeInfo>): SessionRuntimeInfo {
|
|
870
|
+
return {
|
|
871
|
+
runtimeId: 'rt',
|
|
872
|
+
workspaceId: workspace.id,
|
|
873
|
+
sessionPath: SESSION_PATH,
|
|
874
|
+
sessionId: 'session',
|
|
875
|
+
status: 'stopped',
|
|
876
|
+
pid: null,
|
|
877
|
+
error: null,
|
|
878
|
+
activity: null,
|
|
879
|
+
active: false,
|
|
880
|
+
...overrides,
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
test('a stopped active runtime reports stopped even with a live sibling', async () => {
|
|
885
|
+
// Two tabs in the same workspace: the one main resolves for prompts is
|
|
886
|
+
// stopped, a background tab is still running. Reading "any runtime" here
|
|
887
|
+
// reported running, so the first prompt skipped its lazy start and hit a
|
|
888
|
+
// stopped manager.
|
|
889
|
+
workspaceListResult = [WORKSPACE_ONE, WORKSPACE_TWO]
|
|
890
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
891
|
+
useAppStore.setState({
|
|
892
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
893
|
+
workspaces: [WORKSPACE_ONE, WORKSPACE_TWO],
|
|
894
|
+
sessionRuntimes: {
|
|
895
|
+
'rt-active': runtimeIn(WORKSPACE_TWO, { runtimeId: 'rt-active', status: 'stopped', active: true }),
|
|
896
|
+
'rt-background': runtimeIn(WORKSPACE_TWO, {
|
|
897
|
+
runtimeId: 'rt-background',
|
|
898
|
+
sessionPath: REPLACEMENT_SESSION_PATH,
|
|
899
|
+
status: 'running',
|
|
900
|
+
pid: 5,
|
|
901
|
+
activity: 'working',
|
|
902
|
+
}),
|
|
903
|
+
},
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
const ok = await useAppStore.getState().activateWorkspace(WORKSPACE_TWO.id)
|
|
907
|
+
|
|
908
|
+
assert.equal(ok, true)
|
|
909
|
+
const state = useAppStore.getState()
|
|
910
|
+
assert.equal(state.piStatus, 'stopped', 'a background sibling must not mask the stopped active runtime')
|
|
911
|
+
assert.equal(state.sessionLoading, false, 'nothing hydrates while the active runtime is down')
|
|
912
|
+
assert.equal(calls.includes('getMessages'), false)
|
|
913
|
+
})
|
|
914
|
+
|
|
915
|
+
test('the first prompt lazy-starts a workspace whose active runtime is stopped', async () => {
|
|
916
|
+
workspaceListResult = [WORKSPACE_ONE, WORKSPACE_TWO]
|
|
917
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
918
|
+
useAppStore.setState({
|
|
919
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
920
|
+
workspaces: [WORKSPACE_ONE, WORKSPACE_TWO],
|
|
921
|
+
sessionRuntimes: {
|
|
922
|
+
'rt-active': runtimeIn(WORKSPACE_TWO, { runtimeId: 'rt-active', status: 'stopped', active: true }),
|
|
923
|
+
'rt-background': runtimeIn(WORKSPACE_TWO, {
|
|
924
|
+
runtimeId: 'rt-background',
|
|
925
|
+
sessionPath: REPLACEMENT_SESSION_PATH,
|
|
926
|
+
status: 'running',
|
|
927
|
+
pid: 5,
|
|
928
|
+
activity: 'working',
|
|
929
|
+
}),
|
|
930
|
+
},
|
|
931
|
+
})
|
|
932
|
+
await useAppStore.getState().activateWorkspace(WORKSPACE_TWO.id)
|
|
933
|
+
calls.length = 0
|
|
934
|
+
|
|
935
|
+
await useAppStore.getState().sendPrompt('ship it')
|
|
936
|
+
|
|
937
|
+
const startAt = calls.indexOf('pi.start')
|
|
938
|
+
assert.notEqual(startAt, -1, 'the prompt must not be sent into a stopped manager')
|
|
939
|
+
assert.equal(
|
|
940
|
+
calls.findIndex((c) => c.startsWith('prompt:')),
|
|
941
|
+
startAt + 1,
|
|
942
|
+
'the prompt goes out only after startup completes'
|
|
943
|
+
)
|
|
944
|
+
})
|
|
945
|
+
|
|
946
|
+
test('openFolderAsWorkspace skips switch when the dropped folder is already active', async () => {
|
|
947
|
+
workspaceListResult = [WORKSPACE_ONE, WORKSPACE_TWO]
|
|
948
|
+
activeWorkspaceResult = WORKSPACE_TWO
|
|
949
|
+
useAppStore.setState({
|
|
950
|
+
activeWorkspace: WORKSPACE_TWO,
|
|
951
|
+
workspaces: [WORKSPACE_ONE, WORKSPACE_TWO],
|
|
952
|
+
messages: [{ id: 'keep', role: 'user', content: 'already here', timestamp: 0 }],
|
|
953
|
+
piStatus: 'running',
|
|
954
|
+
currentView: 'home',
|
|
955
|
+
})
|
|
956
|
+
|
|
957
|
+
const ok = await useAppStore.getState().openFolderAsWorkspace(WORKSPACE_TWO.path)
|
|
958
|
+
|
|
959
|
+
assert.equal(ok, true)
|
|
960
|
+
assert.equal(
|
|
961
|
+
calls.some((c) => c.startsWith('setActiveWorkspace:')),
|
|
962
|
+
false,
|
|
963
|
+
're-dropping the current project must not tear down the session via switch'
|
|
964
|
+
)
|
|
965
|
+
assert.equal(useAppStore.getState().messages[0]?.id, 'keep')
|
|
966
|
+
assert.equal(useAppStore.getState().currentView, 'chat')
|
|
967
|
+
})
|
|
968
|
+
|
|
969
|
+
// A dropped folder that is already registered should use the normal background-safe
|
|
970
|
+
// workspace switch rather than asking the user to stop the old Pi turn.
|
|
971
|
+
test('dropping an existing workspace switches without a streaming confirmation', async () => {
|
|
972
|
+
workspaceListResult = [WORKSPACE_ONE, WORKSPACE_TWO]
|
|
973
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
974
|
+
useAppStore.setState({
|
|
975
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
976
|
+
workspaces: [WORKSPACE_ONE, WORKSPACE_TWO],
|
|
977
|
+
currentView: 'home',
|
|
978
|
+
})
|
|
979
|
+
enterStreamingState()
|
|
980
|
+
|
|
981
|
+
const ok = await useAppStore.getState().openFolderAsWorkspace(WORKSPACE_TWO.path)
|
|
982
|
+
|
|
983
|
+
assert.equal(ok, true)
|
|
984
|
+
assert.equal(calls.some((c) => c.startsWith('createWorkspace:')), false)
|
|
985
|
+
assert.equal(calls.includes(`setActiveWorkspace:${WORKSPACE_TWO.id}`), true)
|
|
986
|
+
assert.equal(useAppStore.getState().activeWorkspace?.id, WORKSPACE_TWO.id)
|
|
987
|
+
assert.equal(useAppStore.getState().messages.length, 0)
|
|
988
|
+
assert.equal(useAppStore.getState().isStreaming, false)
|
|
989
|
+
})
|
|
990
|
+
|
|
991
|
+
test('openFolderAsWorkspace preserves surrounding whitespace in the folder path', async () => {
|
|
992
|
+
// Legal POSIX folder name with a trailing space; trimming would probe a
|
|
993
|
+
// different, nonexistent path.
|
|
994
|
+
const SPACED_PATH = '/tmp/spaced '
|
|
995
|
+
|
|
996
|
+
const ok = await useAppStore.getState().openFolderAsWorkspace(SPACED_PATH)
|
|
997
|
+
|
|
998
|
+
assert.equal(ok, true)
|
|
999
|
+
assert.equal(
|
|
1000
|
+
calls.includes(`pathKind:${SPACED_PATH}`),
|
|
1001
|
+
true,
|
|
1002
|
+
'the dropped path must reach main exactly as the OS reported it'
|
|
1003
|
+
)
|
|
1004
|
+
})
|
|
1005
|
+
|
|
1006
|
+
// ─── Unsaved-editor guard ────────────────────────────────────────────────────
|
|
1007
|
+
// The editor's dirty flag is mirrored into the store so every path that would
|
|
1008
|
+
// silently destroy the edit buffer — showing another file, closing the editor,
|
|
1009
|
+
// opening the diff pane over it, switching workspace — asks first.
|
|
1010
|
+
|
|
1011
|
+
const CODE_FILE: PreviewTarget = {
|
|
1012
|
+
kind: 'code',
|
|
1013
|
+
name: 'a.ts',
|
|
1014
|
+
path: '/tmp/one/a.ts',
|
|
1015
|
+
relativePath: 'a.ts',
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const OTHER_FILE: PreviewTarget = {
|
|
1019
|
+
kind: 'code',
|
|
1020
|
+
name: 'b.ts',
|
|
1021
|
+
path: '/tmp/one/b.ts',
|
|
1022
|
+
relativePath: 'b.ts',
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
test('editorDirty transitions are mirrored to main for the quit guard', () => {
|
|
1026
|
+
useAppStore.setState({ previewTarget: CODE_FILE })
|
|
1027
|
+
|
|
1028
|
+
useAppStore.getState().setEditorDirty(true)
|
|
1029
|
+
useAppStore.getState().setEditorDirty(true)
|
|
1030
|
+
useAppStore.getState().setEditorDirty(false)
|
|
1031
|
+
// Direct set() writes (how switch/discard actions clear the flag) must
|
|
1032
|
+
// mirror too — main's cache going stale would make quit nag forever.
|
|
1033
|
+
useAppStore.setState({ editorDirty: true })
|
|
1034
|
+
useAppStore.setState({ editorDirty: false })
|
|
1035
|
+
|
|
1036
|
+
assert.deepEqual(
|
|
1037
|
+
calls.filter((c) => c.startsWith('editorDirtyMirror:')),
|
|
1038
|
+
[
|
|
1039
|
+
`editorDirtyMirror:true:${CODE_FILE.name}`,
|
|
1040
|
+
'editorDirtyMirror:false:',
|
|
1041
|
+
`editorDirtyMirror:true:${CODE_FILE.name}`,
|
|
1042
|
+
'editorDirtyMirror:false:',
|
|
1043
|
+
],
|
|
1044
|
+
'exactly one push per transition, with the file name while dirty'
|
|
1045
|
+
)
|
|
1046
|
+
})
|
|
1047
|
+
|
|
1048
|
+
test('setPreviewTarget applies immediately when the editor is clean', async () => {
|
|
1049
|
+
useAppStore.setState({ previewTarget: CODE_FILE })
|
|
1050
|
+
|
|
1051
|
+
const ok = await useAppStore.getState().setPreviewTarget(OTHER_FILE)
|
|
1052
|
+
|
|
1053
|
+
assert.equal(ok, true)
|
|
1054
|
+
assert.equal(useAppStore.getState().previewTarget?.path, OTHER_FILE.path)
|
|
1055
|
+
})
|
|
1056
|
+
|
|
1057
|
+
test('a dirty editor asks before showing another file; declining keeps it', async () => {
|
|
1058
|
+
useAppStore.setState({ previewTarget: CODE_FILE, editorDirty: true })
|
|
1059
|
+
answerConfirm(false)
|
|
1060
|
+
|
|
1061
|
+
const ok = await useAppStore.getState().setPreviewTarget(OTHER_FILE)
|
|
1062
|
+
|
|
1063
|
+
assert.equal(ok, false)
|
|
1064
|
+
assert.equal(
|
|
1065
|
+
useAppStore.getState().previewTarget?.path,
|
|
1066
|
+
CODE_FILE.path,
|
|
1067
|
+
'declining must keep the dirty file on screen'
|
|
1068
|
+
)
|
|
1069
|
+
assert.equal(useAppStore.getState().editorDirty, true)
|
|
1070
|
+
})
|
|
1071
|
+
|
|
1072
|
+
test('accepting the discard applies the new preview and clears the dirty flag', async () => {
|
|
1073
|
+
useAppStore.setState({ previewTarget: CODE_FILE, editorDirty: true })
|
|
1074
|
+
answerConfirm(true)
|
|
1075
|
+
|
|
1076
|
+
const ok = await useAppStore.getState().setPreviewTarget(OTHER_FILE)
|
|
1077
|
+
|
|
1078
|
+
assert.equal(ok, true)
|
|
1079
|
+
assert.equal(useAppStore.getState().previewTarget?.path, OTHER_FILE.path)
|
|
1080
|
+
assert.equal(useAppStore.getState().editorDirty, false)
|
|
1081
|
+
})
|
|
1082
|
+
|
|
1083
|
+
test('re-selecting the same dirty file needs no confirmation and keeps the buffer', async () => {
|
|
1084
|
+
useAppStore.setState({ previewTarget: CODE_FILE, editorDirty: true })
|
|
1085
|
+
|
|
1086
|
+
const ok = await useAppStore.getState().setPreviewTarget({ ...CODE_FILE })
|
|
1087
|
+
|
|
1088
|
+
assert.equal(ok, true)
|
|
1089
|
+
assert.equal(
|
|
1090
|
+
useAppStore.getState().editorDirty,
|
|
1091
|
+
true,
|
|
1092
|
+
'the edit buffer survives a same-file re-select, so the flag must too'
|
|
1093
|
+
)
|
|
1094
|
+
})
|
|
1095
|
+
|
|
1096
|
+
test('closing a dirty editor asks first; declining keeps it open', async () => {
|
|
1097
|
+
useAppStore.setState({ previewTarget: CODE_FILE, editorDirty: true })
|
|
1098
|
+
answerConfirm(false)
|
|
1099
|
+
|
|
1100
|
+
const ok = await useAppStore.getState().setPreviewTarget(null)
|
|
1101
|
+
|
|
1102
|
+
assert.equal(ok, false)
|
|
1103
|
+
assert.equal(useAppStore.getState().previewTarget?.path, CODE_FILE.path)
|
|
1104
|
+
})
|
|
1105
|
+
|
|
1106
|
+
test('opening the diff over a dirty editor asks first; declining keeps the panel', async () => {
|
|
1107
|
+
useAppStore.setState({ previewTarget: CODE_FILE, editorDirty: true, chatSidePanel: null })
|
|
1108
|
+
answerConfirm(false)
|
|
1109
|
+
|
|
1110
|
+
const ok = await useAppStore.getState().setChatSidePanel('diff')
|
|
1111
|
+
|
|
1112
|
+
assert.equal(ok, false)
|
|
1113
|
+
assert.equal(useAppStore.getState().chatSidePanel, null)
|
|
1114
|
+
assert.equal(useAppStore.getState().editorDirty, true)
|
|
1115
|
+
})
|
|
1116
|
+
|
|
1117
|
+
test('accepting the diff-open discards the buffer', async () => {
|
|
1118
|
+
useAppStore.setState({ previewTarget: CODE_FILE, editorDirty: true, chatSidePanel: null })
|
|
1119
|
+
answerConfirm(true)
|
|
1120
|
+
|
|
1121
|
+
const ok = await useAppStore.getState().setChatSidePanel('diff')
|
|
1122
|
+
|
|
1123
|
+
assert.equal(ok, true)
|
|
1124
|
+
assert.equal(useAppStore.getState().chatSidePanel, 'diff')
|
|
1125
|
+
assert.equal(useAppStore.getState().editorDirty, false)
|
|
1126
|
+
})
|
|
1127
|
+
|
|
1128
|
+
test('non-diff panel changes never ask — the editor pane stays mounted', async () => {
|
|
1129
|
+
useAppStore.setState({ previewTarget: CODE_FILE, editorDirty: true, chatSidePanel: null })
|
|
1130
|
+
|
|
1131
|
+
const ok = await useAppStore.getState().setChatSidePanel('files')
|
|
1132
|
+
|
|
1133
|
+
assert.equal(ok, true)
|
|
1134
|
+
assert.equal(useAppStore.getState().chatSidePanel, 'files')
|
|
1135
|
+
assert.equal(useAppStore.getState().editorDirty, true)
|
|
1136
|
+
})
|
|
1137
|
+
|
|
1138
|
+
test('switchWorkspace asks before discarding a dirty editor; declining aborts the switch', async () => {
|
|
1139
|
+
useAppStore.setState({
|
|
1140
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
1141
|
+
previewTarget: CODE_FILE,
|
|
1142
|
+
editorDirty: true,
|
|
1143
|
+
})
|
|
1144
|
+
answerConfirm(false)
|
|
1145
|
+
|
|
1146
|
+
const switched = await useAppStore.getState().switchWorkspace(WORKSPACE_ID)
|
|
1147
|
+
|
|
1148
|
+
assert.equal(switched, false)
|
|
1149
|
+
assert.equal(
|
|
1150
|
+
calls.some((c) => c.startsWith('setActiveWorkspace:')),
|
|
1151
|
+
false,
|
|
1152
|
+
'a declined discard must abort the switch before setActive'
|
|
1153
|
+
)
|
|
1154
|
+
assert.equal(useAppStore.getState().previewTarget?.path, CODE_FILE.path)
|
|
1155
|
+
assert.equal(useAppStore.getState().editorDirty, true)
|
|
1156
|
+
})
|
|
1157
|
+
|
|
1158
|
+
test('accepting the editor discard lets the workspace switch proceed', async () => {
|
|
1159
|
+
useAppStore.setState({
|
|
1160
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
1161
|
+
previewTarget: CODE_FILE,
|
|
1162
|
+
editorDirty: true,
|
|
1163
|
+
})
|
|
1164
|
+
answerConfirm(true)
|
|
1165
|
+
|
|
1166
|
+
const switched = await useAppStore.getState().switchWorkspace(WORKSPACE_ID)
|
|
1167
|
+
|
|
1168
|
+
assert.equal(switched, true)
|
|
1169
|
+
assert.equal(calls.includes(`setActiveWorkspace:${WORKSPACE_ID}`), true)
|
|
1170
|
+
assert.equal(useAppStore.getState().editorDirty, false)
|
|
1171
|
+
})
|
|
1172
|
+
|
|
1173
|
+
test('a committed workspace switch closes the preview', async () => {
|
|
1174
|
+
// The open file belongs to the workspace being left; the new workspace's
|
|
1175
|
+
// file service would refuse to touch it anyway.
|
|
1176
|
+
useAppStore.setState({ activeWorkspace: WORKSPACE_ONE, previewTarget: CODE_FILE })
|
|
1177
|
+
|
|
1178
|
+
const switched = await useAppStore.getState().switchWorkspace(WORKSPACE_ID)
|
|
1179
|
+
|
|
1180
|
+
assert.equal(switched, true)
|
|
1181
|
+
assert.equal(useAppStore.getState().previewTarget, null)
|
|
1182
|
+
})
|
|
1183
|
+
|
|
1184
|
+
test('a main-side activation adoption closes the preview too', async () => {
|
|
1185
|
+
// Same rationale as the committed switch: after main promotes another
|
|
1186
|
+
// workspace (duplicate-path create, active-workspace removal), the file on
|
|
1187
|
+
// screen belongs to a workspace that is no longer active.
|
|
1188
|
+
useAppStore.setState({
|
|
1189
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
1190
|
+
previewTarget: CODE_FILE,
|
|
1191
|
+
editorDirty: true,
|
|
1192
|
+
})
|
|
1193
|
+
activeWorkspaceResult = WORKSPACE_TWO
|
|
1194
|
+
|
|
1195
|
+
await useAppStore.getState().createWorkspace(WORKSPACE_TWO.name, WORKSPACE_TWO.path)
|
|
1196
|
+
|
|
1197
|
+
assert.equal(useAppStore.getState().previewTarget, null)
|
|
1198
|
+
assert.equal(useAppStore.getState().editorDirty, false)
|
|
1199
|
+
})
|
|
1200
|
+
|
|
1201
|
+
test('a chat file link declined by the dirty editor changes nothing', async () => {
|
|
1202
|
+
useAppStore.setState({
|
|
1203
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
1204
|
+
previewTarget: CODE_FILE,
|
|
1205
|
+
editorDirty: true,
|
|
1206
|
+
})
|
|
1207
|
+
fileSearchResults = [
|
|
1208
|
+
{ name: 'b.ts', path: '/tmp/one/b.ts', relativePath: 'b.ts', matchType: 'name' },
|
|
1209
|
+
]
|
|
1210
|
+
answerConfirm(false)
|
|
1211
|
+
|
|
1212
|
+
await openFileFromChat('b.ts')
|
|
1213
|
+
|
|
1214
|
+
assert.equal(useAppStore.getState().previewTarget?.path, CODE_FILE.path)
|
|
1215
|
+
assert.equal(useAppStore.getState().editorDirty, true)
|
|
1216
|
+
})
|
|
1217
|
+
|
|
1218
|
+
test('an accepted chat file link opens the file', async () => {
|
|
1219
|
+
useAppStore.setState({
|
|
1220
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
1221
|
+
previewTarget: CODE_FILE,
|
|
1222
|
+
editorDirty: true,
|
|
1223
|
+
})
|
|
1224
|
+
fileSearchResults = [
|
|
1225
|
+
{ name: 'b.ts', path: '/tmp/one/b.ts', relativePath: 'b.ts', matchType: 'name' },
|
|
1226
|
+
]
|
|
1227
|
+
answerConfirm(true)
|
|
1228
|
+
|
|
1229
|
+
await openFileFromChat('b.ts')
|
|
1230
|
+
|
|
1231
|
+
assert.equal(useAppStore.getState().previewTarget?.path, '/tmp/one/b.ts')
|
|
1232
|
+
assert.equal(useAppStore.getState().editorDirty, false)
|
|
1233
|
+
})
|
|
1234
|
+
|
|
1235
|
+
test('removing a workspace asks first; declining leaves it registered', async () => {
|
|
1236
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
1237
|
+
useAppStore.setState({
|
|
1238
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
1239
|
+
workspaces: [WORKSPACE_ONE, WORKSPACE_TWO],
|
|
1240
|
+
})
|
|
1241
|
+
answerConfirm(false)
|
|
1242
|
+
|
|
1243
|
+
await useAppStore.getState().removeWorkspace(WORKSPACE_TWO.id)
|
|
1244
|
+
|
|
1245
|
+
assert.equal(
|
|
1246
|
+
calls.some((c) => c.startsWith('removeWorkspace:')),
|
|
1247
|
+
false,
|
|
1248
|
+
'a declined removal must never reach main'
|
|
1249
|
+
)
|
|
1250
|
+
})
|
|
1251
|
+
|
|
1252
|
+
test('removing the active workspace with a dirty editor asks about the edits too', async () => {
|
|
1253
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
1254
|
+
useAppStore.setState({
|
|
1255
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
1256
|
+
workspaces: [WORKSPACE_ONE, WORKSPACE_TWO],
|
|
1257
|
+
previewTarget: CODE_FILE,
|
|
1258
|
+
editorDirty: true,
|
|
1259
|
+
})
|
|
1260
|
+
// Yes to the removal, no to discarding the edits.
|
|
1261
|
+
answerConfirms([true, false])
|
|
1262
|
+
|
|
1263
|
+
await useAppStore.getState().removeWorkspace(WORKSPACE_ONE.id)
|
|
1264
|
+
|
|
1265
|
+
assert.equal(
|
|
1266
|
+
calls.some((c) => c.startsWith('removeWorkspace:')),
|
|
1267
|
+
false,
|
|
1268
|
+
'declining the discard must abort the removal'
|
|
1269
|
+
)
|
|
1270
|
+
assert.equal(useAppStore.getState().previewTarget?.path, CODE_FILE.path)
|
|
1271
|
+
assert.equal(useAppStore.getState().editorDirty, true)
|
|
1272
|
+
})
|
|
1273
|
+
|
|
1274
|
+
test('removing an inactive workspace never asks about the editor', async () => {
|
|
1275
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
1276
|
+
useAppStore.setState({
|
|
1277
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
1278
|
+
workspaces: [WORKSPACE_ONE, WORKSPACE_TWO],
|
|
1279
|
+
previewTarget: CODE_FILE,
|
|
1280
|
+
editorDirty: true,
|
|
1281
|
+
})
|
|
1282
|
+
// Only the removal dialog; an unexpected second dialog would hang the test.
|
|
1283
|
+
answerConfirm(true)
|
|
1284
|
+
|
|
1285
|
+
await useAppStore.getState().removeWorkspace(WORKSPACE_TWO.id)
|
|
1286
|
+
|
|
1287
|
+
assert.equal(calls.includes(`removeWorkspace:${WORKSPACE_TWO.id}`), true)
|
|
1288
|
+
assert.equal(useAppStore.getState().previewTarget?.path, CODE_FILE.path)
|
|
1289
|
+
assert.equal(useAppStore.getState().editorDirty, true)
|
|
1290
|
+
})
|
|
1291
|
+
|
|
1292
|
+
test('creating a duplicate-path workspace asks before activating over a dirty editor', async () => {
|
|
1293
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
1294
|
+
workspaceListResult = [WORKSPACE_ONE, WORKSPACE_TWO]
|
|
1295
|
+
useAppStore.setState({
|
|
1296
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
1297
|
+
workspaces: [WORKSPACE_ONE, WORKSPACE_TWO],
|
|
1298
|
+
previewTarget: CODE_FILE,
|
|
1299
|
+
editorDirty: true,
|
|
1300
|
+
})
|
|
1301
|
+
answerConfirm(false)
|
|
1302
|
+
|
|
1303
|
+
await useAppStore.getState().createWorkspace(WORKSPACE_TWO.name, WORKSPACE_TWO.path)
|
|
1304
|
+
|
|
1305
|
+
assert.equal(
|
|
1306
|
+
calls.some((c) => c.startsWith('createWorkspace:')),
|
|
1307
|
+
false,
|
|
1308
|
+
'main activates a duplicate path inside create, so the ask must come before the IPC'
|
|
1309
|
+
)
|
|
1310
|
+
assert.equal(useAppStore.getState().activeWorkspace?.id, WORKSPACE_ONE.id)
|
|
1311
|
+
assert.equal(useAppStore.getState().editorDirty, true)
|
|
1312
|
+
})
|
|
1313
|
+
|
|
1314
|
+
test('a duplicate-path create routes through the full workspace switch', async () => {
|
|
1315
|
+
workspaceListResult = [WORKSPACE_ONE, WORKSPACE_TWO]
|
|
1316
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
1317
|
+
useAppStore.setState({
|
|
1318
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
1319
|
+
workspaces: [WORKSPACE_ONE, WORKSPACE_TWO],
|
|
1320
|
+
messages: [{ id: 'old', role: 'user', content: 'from workspace one', timestamp: 0 }],
|
|
1321
|
+
})
|
|
1322
|
+
|
|
1323
|
+
await useAppStore.getState().createWorkspace(WORKSPACE_TWO.name, WORKSPACE_TWO.path)
|
|
1324
|
+
|
|
1325
|
+
assert.equal(
|
|
1326
|
+
calls.some((c) => c.startsWith('createWorkspace:')),
|
|
1327
|
+
false,
|
|
1328
|
+
'main would activate inside create, skipping the switch teardown'
|
|
1329
|
+
)
|
|
1330
|
+
assert.equal(calls.includes(`setActiveWorkspace:${WORKSPACE_TWO.id}`), true)
|
|
1331
|
+
assert.deepEqual(
|
|
1332
|
+
useAppStore.getState().messages,
|
|
1333
|
+
[],
|
|
1334
|
+
'the previous workspace chat must clear like any other switch'
|
|
1335
|
+
)
|
|
1336
|
+
})
|
|
1337
|
+
|
|
1338
|
+
test('a duplicate-path create switches while Pi keeps working in the background', async () => {
|
|
1339
|
+
workspaceListResult = [WORKSPACE_ONE, WORKSPACE_TWO]
|
|
1340
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
1341
|
+
useAppStore.setState({
|
|
1342
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
1343
|
+
workspaces: [WORKSPACE_ONE, WORKSPACE_TWO],
|
|
1344
|
+
})
|
|
1345
|
+
enterStreamingState()
|
|
1346
|
+
|
|
1347
|
+
await useAppStore.getState().createWorkspace(WORKSPACE_TWO.name, WORKSPACE_TWO.path)
|
|
1348
|
+
|
|
1349
|
+
assert.equal(calls.some((c) => c.startsWith('createWorkspace:')), false)
|
|
1350
|
+
assert.equal(calls.includes(`setActiveWorkspace:${WORKSPACE_TWO.id}`), true)
|
|
1351
|
+
assert.equal(useAppStore.getState().isStreaming, false)
|
|
1352
|
+
})
|
|
1353
|
+
|
|
1354
|
+
test('creating the already-active path is a no-op', async () => {
|
|
1355
|
+
workspaceListResult = [WORKSPACE_ONE]
|
|
1356
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
1357
|
+
useAppStore.setState({
|
|
1358
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
1359
|
+
workspaces: [WORKSPACE_ONE],
|
|
1360
|
+
messages: [{ id: 'keep', role: 'user', content: 'stay', timestamp: 0 }],
|
|
1361
|
+
})
|
|
1362
|
+
|
|
1363
|
+
await useAppStore.getState().createWorkspace(WORKSPACE_ONE.name, WORKSPACE_ONE.path)
|
|
1364
|
+
|
|
1365
|
+
assert.equal(calls.length, 0, 'nothing to create and nothing to switch')
|
|
1366
|
+
assert.equal(useAppStore.getState().messages[0]?.id, 'keep')
|
|
1367
|
+
})
|
|
1368
|
+
|
|
1369
|
+
test('a new-path create leaves a dirty editor alone', async () => {
|
|
1370
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
1371
|
+
workspaceListResult = [WORKSPACE_ONE]
|
|
1372
|
+
useAppStore.setState({
|
|
1373
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
1374
|
+
workspaces: [WORKSPACE_ONE],
|
|
1375
|
+
previewTarget: CODE_FILE,
|
|
1376
|
+
editorDirty: true,
|
|
1377
|
+
})
|
|
1378
|
+
|
|
1379
|
+
await useAppStore.getState().createWorkspace('fresh', '/tmp/fresh')
|
|
1380
|
+
|
|
1381
|
+
assert.equal(calls.includes('createWorkspace:fresh:/tmp/fresh'), true)
|
|
1382
|
+
assert.equal(useAppStore.getState().previewTarget?.path, CODE_FILE.path)
|
|
1383
|
+
assert.equal(useAppStore.getState().editorDirty, true)
|
|
1384
|
+
})
|
|
1385
|
+
|
|
1386
|
+
test('changing the active workspace folder asks a dirty editor first', async () => {
|
|
1387
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
1388
|
+
useAppStore.setState({
|
|
1389
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
1390
|
+
previewTarget: CODE_FILE,
|
|
1391
|
+
editorDirty: true,
|
|
1392
|
+
})
|
|
1393
|
+
answerConfirm(false)
|
|
1394
|
+
|
|
1395
|
+
await useAppStore.getState().changeWorkspaceFolder(WORKSPACE_ONE.id, '/tmp/elsewhere')
|
|
1396
|
+
|
|
1397
|
+
assert.equal(
|
|
1398
|
+
calls.some((c) => c.startsWith('changePath:')),
|
|
1399
|
+
false,
|
|
1400
|
+
'a declined discard must leave the folder unchanged'
|
|
1401
|
+
)
|
|
1402
|
+
assert.equal(useAppStore.getState().previewTarget?.path, CODE_FILE.path)
|
|
1403
|
+
assert.equal(useAppStore.getState().editorDirty, true)
|
|
1404
|
+
})
|
|
1405
|
+
|
|
1406
|
+
test('an accepted active-folder change closes the preview', async () => {
|
|
1407
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
1408
|
+
useAppStore.setState({
|
|
1409
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
1410
|
+
previewTarget: CODE_FILE,
|
|
1411
|
+
editorDirty: true,
|
|
1412
|
+
})
|
|
1413
|
+
answerConfirm(true)
|
|
1414
|
+
|
|
1415
|
+
await useAppStore.getState().changeWorkspaceFolder(WORKSPACE_ONE.id, '/tmp/elsewhere')
|
|
1416
|
+
|
|
1417
|
+
assert.equal(calls.includes(`changePath:${WORKSPACE_ONE.id}:/tmp/elsewhere`), true)
|
|
1418
|
+
assert.equal(
|
|
1419
|
+
useAppStore.getState().previewTarget,
|
|
1420
|
+
null,
|
|
1421
|
+
'the open file binds the old folder and is unsaveable under the new root'
|
|
1422
|
+
)
|
|
1423
|
+
assert.equal(useAppStore.getState().editorDirty, false)
|
|
1424
|
+
})
|
|
1425
|
+
|
|
1426
|
+
test('changing the active folder warns while Pi is streaming', async () => {
|
|
1427
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
1428
|
+
useAppStore.setState({ activeWorkspace: WORKSPACE_ONE })
|
|
1429
|
+
enterStreamingState()
|
|
1430
|
+
answerConfirm(false)
|
|
1431
|
+
|
|
1432
|
+
await useAppStore.getState().changeWorkspaceFolder(WORKSPACE_ONE.id, '/tmp/elsewhere')
|
|
1433
|
+
|
|
1434
|
+
assert.equal(
|
|
1435
|
+
calls.some((c) => c.startsWith('changePath:')),
|
|
1436
|
+
false,
|
|
1437
|
+
'the restart below would kill the in-flight turn, so declining must abort'
|
|
1438
|
+
)
|
|
1439
|
+
assert.equal(useAppStore.getState().isStreaming, true)
|
|
1440
|
+
})
|
|
1441
|
+
|
|
1442
|
+
test('an accepted active-folder change restarts a running Pi', async () => {
|
|
1443
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
1444
|
+
useAppStore.setState({ activeWorkspace: WORKSPACE_ONE, piStatus: 'running' })
|
|
1445
|
+
|
|
1446
|
+
await useAppStore.getState().changeWorkspaceFolder(WORKSPACE_ONE.id, '/tmp/elsewhere')
|
|
1447
|
+
|
|
1448
|
+
assert.equal(calls.includes(`changePath:${WORKSPACE_ONE.id}:/tmp/elsewhere`), true)
|
|
1449
|
+
assert.equal(
|
|
1450
|
+
calls.includes('pi.restart'),
|
|
1451
|
+
true,
|
|
1452
|
+
"Pi's cwd is bound at spawn — without a restart it keeps working in the old folder"
|
|
1453
|
+
)
|
|
1454
|
+
})
|
|
1455
|
+
|
|
1456
|
+
test('a stopped Pi is not restarted by a folder change', async () => {
|
|
1457
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
1458
|
+
useAppStore.setState({ activeWorkspace: WORKSPACE_ONE, piStatus: 'stopped' })
|
|
1459
|
+
|
|
1460
|
+
await useAppStore.getState().changeWorkspaceFolder(WORKSPACE_ONE.id, '/tmp/elsewhere')
|
|
1461
|
+
|
|
1462
|
+
assert.equal(calls.includes(`changePath:${WORKSPACE_ONE.id}:/tmp/elsewhere`), true)
|
|
1463
|
+
assert.equal(calls.includes('pi.restart'), false)
|
|
1464
|
+
})
|
|
1465
|
+
|
|
1466
|
+
test('changing an inactive workspace folder touches neither dialog nor preview', async () => {
|
|
1467
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
1468
|
+
useAppStore.setState({
|
|
1469
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
1470
|
+
previewTarget: CODE_FILE,
|
|
1471
|
+
editorDirty: true,
|
|
1472
|
+
})
|
|
1473
|
+
|
|
1474
|
+
await useAppStore.getState().changeWorkspaceFolder(WORKSPACE_TWO.id, '/tmp/elsewhere')
|
|
1475
|
+
|
|
1476
|
+
assert.equal(calls.includes(`changePath:${WORKSPACE_TWO.id}:/tmp/elsewhere`), true)
|
|
1477
|
+
assert.equal(useAppStore.getState().previewTarget?.path, CODE_FILE.path)
|
|
1478
|
+
assert.equal(useAppStore.getState().editorDirty, true)
|
|
1479
|
+
})
|
|
1480
|
+
|
|
1481
|
+
test('a chat file link closes a diff pane opened while its search was in flight', async () => {
|
|
1482
|
+
useAppStore.setState({ activeWorkspace: WORKSPACE_ONE, chatSidePanel: null })
|
|
1483
|
+
fileSearchResults = [
|
|
1484
|
+
{ name: 'b.ts', path: '/tmp/one/b.ts', relativePath: 'b.ts', matchType: 'name' },
|
|
1485
|
+
]
|
|
1486
|
+
// The user opens the diff pane while the search IPC is still out — the
|
|
1487
|
+
// pane check must read current state, not the pre-await snapshot.
|
|
1488
|
+
fileSearchHook = () => {
|
|
1489
|
+
useAppStore.setState({ chatSidePanel: 'diff' })
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
await openFileFromChat('b.ts')
|
|
1493
|
+
|
|
1494
|
+
assert.equal(useAppStore.getState().previewTarget?.path, '/tmp/one/b.ts')
|
|
1495
|
+
assert.equal(
|
|
1496
|
+
useAppStore.getState().chatSidePanel,
|
|
1497
|
+
null,
|
|
1498
|
+
'the diff pane must make way for the preview it would otherwise hide'
|
|
1499
|
+
)
|
|
1500
|
+
})
|
|
1501
|
+
|
|
1502
|
+
// ─── Cross-workspace session open (skipSessionLoad contract) ─────────────────
|
|
1503
|
+
// The sidebar/session-panel flow is: switchWorkspace({skipSessionLoad}) then
|
|
1504
|
+
// switchSession(target). The workspace switch clears the chat; the target is
|
|
1505
|
+
// very often the new workspace's remembered active session, so the fast path
|
|
1506
|
+
// must not eat the follow-up click.
|
|
1507
|
+
|
|
1508
|
+
test('switchSession loads history even when sessionState already names the target', async () => {
|
|
1509
|
+
// Post-workspace-switch state: chat cleared, sessionState already refreshed
|
|
1510
|
+
// to the very session the user clicked.
|
|
1511
|
+
useAppStore.setState({
|
|
1512
|
+
sessionState: sessionStateWith(SESSION_PATH),
|
|
1513
|
+
messages: [],
|
|
1514
|
+
sessionLoading: false,
|
|
1515
|
+
})
|
|
1516
|
+
|
|
1517
|
+
await useAppStore.getState().switchSession(SESSION_PATH)
|
|
1518
|
+
|
|
1519
|
+
assert.equal(
|
|
1520
|
+
calls.includes(`switch:${SESSION_PATH}`),
|
|
1521
|
+
true,
|
|
1522
|
+
'an empty chat means nothing is on screen — the session must load'
|
|
1523
|
+
)
|
|
1524
|
+
})
|
|
1525
|
+
|
|
1526
|
+
test('switchSession still skips a reload when the session is already on screen', async () => {
|
|
1527
|
+
useAppStore.setState({
|
|
1528
|
+
sessionState: sessionStateWith(SESSION_PATH),
|
|
1529
|
+
messages: [{ id: 'm1', role: 'user', content: 'hi', timestamp: 0 }],
|
|
1530
|
+
sessionLoading: false,
|
|
1531
|
+
})
|
|
1532
|
+
|
|
1533
|
+
await useAppStore.getState().switchSession(SESSION_PATH)
|
|
1534
|
+
|
|
1535
|
+
assert.equal(calls.includes(`switch:${SESSION_PATH}`), false)
|
|
1536
|
+
})
|
|
1537
|
+
|
|
1538
|
+
test('the cross-workspace open flow loads the clicked session end to end', async () => {
|
|
1539
|
+
workspaceListResult = [WORKSPACE_ONE, WORKSPACE_TWO]
|
|
1540
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
1541
|
+
useAppStore.setState({
|
|
1542
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
1543
|
+
workspaces: [WORKSPACE_ONE, WORKSPACE_TWO],
|
|
1544
|
+
})
|
|
1545
|
+
// The target workspace's Pi resumes straight onto the very session the user
|
|
1546
|
+
// clicked (startPi refreshes sessionState to it) — the arrangement whose
|
|
1547
|
+
// refresh/fast-path race used to eat the click and leave an empty chat.
|
|
1548
|
+
sessionStateResult = sessionStateWith(SESSION_PATH)
|
|
1549
|
+
|
|
1550
|
+
const ok = await useAppStore.getState().switchWorkspace(WORKSPACE_ID, { skipSessionLoad: true })
|
|
1551
|
+
assert.equal(ok, true)
|
|
1552
|
+
await useAppStore.getState().switchSession(SESSION_PATH)
|
|
1553
|
+
|
|
1554
|
+
assert.equal(
|
|
1555
|
+
calls.includes(`switch:${SESSION_PATH}`),
|
|
1556
|
+
true,
|
|
1557
|
+
'the chat was cleared by the workspace switch, so the session must actually load'
|
|
1558
|
+
)
|
|
1559
|
+
})
|
|
1560
|
+
|
|
1561
|
+
// ─── openSessionItem (shared sidebar / session-panel / quick-switcher flow) ──
|
|
1562
|
+
|
|
1563
|
+
function sessionItemFor(workspace: Workspace): SessionListItem {
|
|
1564
|
+
return {
|
|
1565
|
+
path: SESSION_PATH,
|
|
1566
|
+
name: 'target session',
|
|
1567
|
+
preview: null,
|
|
1568
|
+
sessionId: 'target-1',
|
|
1569
|
+
lastModified: 0,
|
|
1570
|
+
messageCount: 1,
|
|
1571
|
+
projectPath: workspace.path,
|
|
1572
|
+
projectName: workspace.name,
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
test('openSessionItem in the active workspace switches straight to the session', async () => {
|
|
1577
|
+
workspaceListResult = [WORKSPACE_ONE]
|
|
1578
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
1579
|
+
useAppStore.setState({
|
|
1580
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
1581
|
+
workspaces: [WORKSPACE_ONE],
|
|
1582
|
+
currentView: 'sessions',
|
|
1583
|
+
})
|
|
1584
|
+
|
|
1585
|
+
await useAppStore.getState().openSessionItem(sessionItemFor(WORKSPACE_ONE))
|
|
1586
|
+
|
|
1587
|
+
assert.equal(calls.some((c) => c.startsWith('setActiveWorkspace')), false)
|
|
1588
|
+
assert.equal(calls.includes(`switch:${SESSION_PATH}`), true)
|
|
1589
|
+
assert.equal(useAppStore.getState().currentView, 'chat')
|
|
1590
|
+
})
|
|
1591
|
+
|
|
1592
|
+
test('openSessionItem auto-switches to the owning workspace first', async () => {
|
|
1593
|
+
workspaceListResult = [WORKSPACE_ONE, WORKSPACE_TWO]
|
|
1594
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
1595
|
+
useAppStore.setState({
|
|
1596
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
1597
|
+
workspaces: [WORKSPACE_ONE, WORKSPACE_TWO],
|
|
1598
|
+
currentView: 'sessions',
|
|
1599
|
+
})
|
|
1600
|
+
|
|
1601
|
+
await useAppStore.getState().openSessionItem(sessionItemFor(WORKSPACE_TWO))
|
|
1602
|
+
|
|
1603
|
+
assert.equal(calls.includes(`setActiveWorkspace:${WORKSPACE_TWO.id}`), true)
|
|
1604
|
+
assert.equal(calls.includes(`switch:${SESSION_PATH}`), true)
|
|
1605
|
+
assert.equal(useAppStore.getState().currentView, 'chat')
|
|
1606
|
+
})
|
|
1607
|
+
|
|
1608
|
+
// ─── Mid-turn re-attach on workspace switch-back ─────────────────────────────
|
|
1609
|
+
|
|
1610
|
+
function enterWorkspacesWithBackgroundTurn(): void {
|
|
1611
|
+
workspaceListResult = [WORKSPACE_ONE, WORKSPACE_TWO]
|
|
1612
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
1613
|
+
// The background turn lives in WORKSPACE_TWO's own process, so main reports
|
|
1614
|
+
// that workspace's manager as running and its activity as working.
|
|
1615
|
+
piStatusResult = 'running'
|
|
1616
|
+
useAppStore.setState({
|
|
1617
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
1618
|
+
workspaces: [WORKSPACE_ONE, WORKSPACE_TWO],
|
|
1619
|
+
workspaceActivity: { [WORKSPACE_ID]: { state: 'working', since: 1 } },
|
|
1620
|
+
})
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
test('openSessionItem switches tabs before opening the requested session', async () => {
|
|
1624
|
+
workspaceListResult = [WORKSPACE_ONE, WORKSPACE_TWO]
|
|
1625
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
1626
|
+
useAppStore.setState({
|
|
1627
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
1628
|
+
workspaces: [WORKSPACE_ONE, WORKSPACE_TWO],
|
|
1629
|
+
currentView: 'sessions',
|
|
1630
|
+
})
|
|
1631
|
+
enterStreamingState()
|
|
1632
|
+
|
|
1633
|
+
await useAppStore.getState().openSessionItem(sessionItemFor(WORKSPACE_TWO))
|
|
1634
|
+
|
|
1635
|
+
assert.equal(calls.includes(`setActiveWorkspace:${WORKSPACE_TWO.id}`), true)
|
|
1636
|
+
assert.equal(calls.includes(`switch:${SESSION_PATH}`), true)
|
|
1637
|
+
assert.equal(useAppStore.getState().currentView, 'chat')
|
|
1638
|
+
})
|
|
1639
|
+
|
|
1640
|
+
test('openSessionItem creates a workspace for an unknown project path', async () => {
|
|
1641
|
+
workspaceListResult = [WORKSPACE_ONE]
|
|
1642
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
1643
|
+
useAppStore.setState({
|
|
1644
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
1645
|
+
workspaces: [WORKSPACE_ONE],
|
|
1646
|
+
currentView: 'sessions',
|
|
1647
|
+
})
|
|
1648
|
+
|
|
1649
|
+
await useAppStore.getState().openSessionItem(sessionItemFor(WORKSPACE_TWO))
|
|
1650
|
+
|
|
1651
|
+
assert.equal(
|
|
1652
|
+
calls.includes(`createWorkspace:${WORKSPACE_TWO.name}:${WORKSPACE_TWO.path}`),
|
|
1653
|
+
true,
|
|
1654
|
+
'an unknown project path must get a workspace before the session switch'
|
|
1655
|
+
)
|
|
1656
|
+
assert.equal(calls.includes(`switch:${SESSION_PATH}`), true)
|
|
1657
|
+
})
|
|
1658
|
+
|
|
1659
|
+
test('switching into a working workspace shows the indicator and marks the attach', async () => {
|
|
1660
|
+
enterWorkspacesWithBackgroundTurn()
|
|
1661
|
+
|
|
1662
|
+
const ok = await useAppStore.getState().switchWorkspace(WORKSPACE_ID)
|
|
1663
|
+
|
|
1664
|
+
assert.equal(ok, true)
|
|
1665
|
+
const state = useAppStore.getState()
|
|
1666
|
+
assert.equal(state.isStreaming, true, 'a running background turn must not render as an idle chat')
|
|
1667
|
+
assert.equal(state.reattachedMidTurn, true)
|
|
1668
|
+
})
|
|
1669
|
+
|
|
1670
|
+
test('switching into an idle workspace attaches nothing', async () => {
|
|
1671
|
+
enterWorkspacesWithBackgroundTurn()
|
|
1672
|
+
useAppStore.setState({ workspaceActivity: {} })
|
|
1673
|
+
|
|
1674
|
+
await useAppStore.getState().switchWorkspace(WORKSPACE_ID)
|
|
1675
|
+
|
|
1676
|
+
const state = useAppStore.getState()
|
|
1677
|
+
assert.equal(state.isStreaming, false)
|
|
1678
|
+
assert.equal(state.reattachedMidTurn, false)
|
|
1679
|
+
})
|
|
1680
|
+
|
|
1681
|
+
test('reopening a working session keeps the mid-turn attach armed', async () => {
|
|
1682
|
+
// The runtime main hands back is already mid-turn. The hydration that
|
|
1683
|
+
// follows tears the chat down (clearMessages resets every per-turn field),
|
|
1684
|
+
// so the attach must be armed after it — otherwise the reply streams into a
|
|
1685
|
+
// chat that looks idle and the turn end commits only the post-switch suffix.
|
|
1686
|
+
workspaceListResult = [WORKSPACE_TWO]
|
|
1687
|
+
activeWorkspaceResult = WORKSPACE_TWO
|
|
1688
|
+
useAppStore.setState({ activeWorkspace: WORKSPACE_TWO, workspaces: [WORKSPACE_TWO] })
|
|
1689
|
+
switchResult = runtimeIn(WORKSPACE_TWO, {
|
|
1690
|
+
runtimeId: 'rt-working',
|
|
1691
|
+
sessionId: 'session-working',
|
|
1692
|
+
status: 'running',
|
|
1693
|
+
pid: 11,
|
|
1694
|
+
activity: 'working',
|
|
1695
|
+
active: true,
|
|
1696
|
+
})
|
|
1697
|
+
|
|
1698
|
+
await useAppStore.getState().switchSession(SESSION_PATH, WORKSPACE_TWO.path)
|
|
1699
|
+
// The hydration switchSession fires is not awaited; let it settle so the
|
|
1700
|
+
// assertions see the state the user is left looking at.
|
|
1701
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
1702
|
+
|
|
1703
|
+
const state = useAppStore.getState()
|
|
1704
|
+
assert.equal(calls.includes('getMessages'), true, 'the persisted history still has to load')
|
|
1705
|
+
assert.equal(state.isStreaming, true, 'a session reopened mid-turn must not render as idle')
|
|
1706
|
+
assert.equal(state.reattachedMidTurn, true, 'the turn end has to backfill the prefix the stream missed')
|
|
1707
|
+
})
|
|
1708
|
+
|
|
1709
|
+
test('reopening an idle session arms no attach', async () => {
|
|
1710
|
+
workspaceListResult = [WORKSPACE_TWO]
|
|
1711
|
+
activeWorkspaceResult = WORKSPACE_TWO
|
|
1712
|
+
useAppStore.setState({ activeWorkspace: WORKSPACE_TWO, workspaces: [WORKSPACE_TWO] })
|
|
1713
|
+
switchResult = runtimeIn(WORKSPACE_TWO, {
|
|
1714
|
+
runtimeId: 'rt-idle',
|
|
1715
|
+
sessionId: 'session-idle',
|
|
1716
|
+
status: 'running',
|
|
1717
|
+
pid: 12,
|
|
1718
|
+
active: true,
|
|
1719
|
+
})
|
|
1720
|
+
|
|
1721
|
+
await useAppStore.getState().switchSession(SESSION_PATH, WORKSPACE_TWO.path)
|
|
1722
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
1723
|
+
|
|
1724
|
+
const state = useAppStore.getState()
|
|
1725
|
+
assert.equal(state.isStreaming, false)
|
|
1726
|
+
assert.equal(state.reattachedMidTurn, false, 'an idle runtime must not trigger a backfill on the next turn')
|
|
1727
|
+
})
|
|
1728
|
+
|
|
1729
|
+
test('agent_end after a mid-turn attach backfills from the session', async () => {
|
|
1730
|
+
enterWorkspacesWithBackgroundTurn()
|
|
1731
|
+
await useAppStore.getState().switchWorkspace(WORKSPACE_ID)
|
|
1732
|
+
const loadsBefore = calls.filter((c) => c === 'getMessages').length
|
|
1733
|
+
|
|
1734
|
+
useAppStore.getState().handlePiEvent({ type: 'agent_end', messages: [] })
|
|
1735
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
1736
|
+
|
|
1737
|
+
const state = useAppStore.getState()
|
|
1738
|
+
assert.equal(state.reattachedMidTurn, false)
|
|
1739
|
+
assert.equal(state.isStreaming, false)
|
|
1740
|
+
assert.equal(
|
|
1741
|
+
calls.filter((c) => c === 'getMessages').length,
|
|
1742
|
+
loadsBefore + 1,
|
|
1743
|
+
'the stream buffers missed the pre-attach output — the session is the source of truth'
|
|
1744
|
+
)
|
|
1745
|
+
})
|
|
1746
|
+
|
|
1747
|
+
test('the activity map going quiet after an attach stops the indicator and backfills', async () => {
|
|
1748
|
+
enterWorkspacesWithBackgroundTurn()
|
|
1749
|
+
await useAppStore.getState().switchWorkspace(WORKSPACE_ID)
|
|
1750
|
+
const loadsBefore = calls.filter((c) => c === 'getMessages').length
|
|
1751
|
+
|
|
1752
|
+
// The turn ended during the switch: its agent_end was filtered while the
|
|
1753
|
+
// manager was not yet active, so only the activity broadcast reports it.
|
|
1754
|
+
useAppStore.getState().handleWorkspaceActivity({})
|
|
1755
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
1756
|
+
|
|
1757
|
+
const state = useAppStore.getState()
|
|
1758
|
+
assert.equal(state.isStreaming, false)
|
|
1759
|
+
assert.equal(state.reattachedMidTurn, false)
|
|
1760
|
+
assert.equal(calls.filter((c) => c === 'getMessages').length, loadsBefore + 1)
|
|
1761
|
+
})
|
|
1762
|
+
|
|
1763
|
+
test('opening a mid-turn session row leaves the other runtime working', async () => {
|
|
1764
|
+
enterWorkspacesWithBackgroundTurn()
|
|
1765
|
+
sessionStateResult = sessionStateWith(SESSION_PATH)
|
|
1766
|
+
switchResult = {
|
|
1767
|
+
runtimeId: 'rt-opened',
|
|
1768
|
+
workspaceId: WORKSPACE_TWO.id,
|
|
1769
|
+
sessionPath: SESSION_PATH,
|
|
1770
|
+
sessionId: 'session-2',
|
|
1771
|
+
status: 'stopped',
|
|
1772
|
+
pid: null,
|
|
1773
|
+
error: null,
|
|
1774
|
+
activity: null,
|
|
1775
|
+
active: true,
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
await useAppStore.getState().openSessionItem(sessionItemFor(WORKSPACE_TWO))
|
|
1779
|
+
|
|
1780
|
+
const state = useAppStore.getState()
|
|
1781
|
+
assert.equal(calls.includes(`switch:${SESSION_PATH}`), true)
|
|
1782
|
+
assert.equal(state.sessionLoading, true, 'hydration defers until the runtime reports running')
|
|
1783
|
+
assert.equal(state.isStreaming, false, 'the selected session starts idle until its own events arrive')
|
|
1784
|
+
|
|
1785
|
+
// Main finishes spawning the selected runtime; its running event hydrates.
|
|
1786
|
+
useAppStore.getState().handleSessionRuntime({
|
|
1787
|
+
runtimeId: 'rt-opened',
|
|
1788
|
+
workspaceId: WORKSPACE_TWO.id,
|
|
1789
|
+
sessionPath: SESSION_PATH,
|
|
1790
|
+
sessionId: 'session-2',
|
|
1791
|
+
status: 'running',
|
|
1792
|
+
pid: 7,
|
|
1793
|
+
error: null,
|
|
1794
|
+
activity: null,
|
|
1795
|
+
active: true,
|
|
1796
|
+
})
|
|
1797
|
+
// Flush the microtask chain the fire-and-forget hydration rides on — no
|
|
1798
|
+
// wall-clock wait needed for a mocked getMessages.
|
|
1799
|
+
await Promise.resolve()
|
|
1800
|
+
await Promise.resolve()
|
|
1801
|
+
|
|
1802
|
+
assert.equal(calls.includes('getMessages'), true, 'the running event hydrates history')
|
|
1803
|
+
assert.equal(useAppStore.getState().sessionLoading, false)
|
|
1804
|
+
})
|
|
1805
|
+
|
|
1806
|
+
test('opening a different session mid-turn does not prompt or stop the other runtime', async () => {
|
|
1807
|
+
enterWorkspacesWithBackgroundTurn()
|
|
1808
|
+
sessionStateResult = sessionStateWith('/tmp/other-turn-session.jsonl')
|
|
1809
|
+
|
|
1810
|
+
await useAppStore.getState().openSessionItem(sessionItemFor(WORKSPACE_TWO))
|
|
1811
|
+
|
|
1812
|
+
assert.equal(calls.includes(`switch:${SESSION_PATH}`), true)
|
|
1813
|
+
assert.equal(useAppStore.getState().reattachedMidTurn, false)
|
|
1814
|
+
})
|
|
1815
|
+
|
|
1816
|
+
test('a background runtime never blocks session navigation with a warning', async () => {
|
|
1817
|
+
enterWorkspacesWithBackgroundTurn()
|
|
1818
|
+
sessionStateResult = sessionStateWith('/tmp/other-turn-session.jsonl')
|
|
1819
|
+
|
|
1820
|
+
await useAppStore.getState().openSessionItem(sessionItemFor(WORKSPACE_TWO))
|
|
1821
|
+
|
|
1822
|
+
assert.equal(calls.includes(`switch:${SESSION_PATH}`), true)
|
|
1823
|
+
assert.equal(useAppStore.getState().confirmRequest, null)
|
|
1824
|
+
})
|
|
1825
|
+
|
|
1826
|
+
test('a mid-turn message_end keeps the attach armed and restores the indicator', async () => {
|
|
1827
|
+
enterWorkspacesWithBackgroundTurn()
|
|
1828
|
+
await useAppStore.getState().switchWorkspace(WORKSPACE_ID)
|
|
1829
|
+
const loadsBefore = calls.filter((c) => c === 'getMessages').length
|
|
1830
|
+
|
|
1831
|
+
// The in-flight message completes but the TURN continues (tool-using
|
|
1832
|
+
// turns have several messages). The backfill's teardown clears the
|
|
1833
|
+
// indicator; it must come back, and the attach must stay armed so the
|
|
1834
|
+
// kill-gates keep warning and later boundaries keep backfilling.
|
|
1835
|
+
useAppStore.getState().handlePiEvent({ type: 'message_end', message: {} })
|
|
1836
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
1837
|
+
|
|
1838
|
+
const state = useAppStore.getState()
|
|
1839
|
+
assert.equal(state.reattachedMidTurn, true)
|
|
1840
|
+
assert.equal(state.isStreaming, true, 'the turn is still running — the UI must not look idle')
|
|
1841
|
+
assert.equal(calls.filter((c) => c === 'getMessages').length, loadsBefore + 1)
|
|
1842
|
+
})
|
|
1843
|
+
|
|
1844
|
+
test('an activity broadcast arms the attach for a renderer that booted mid-turn', async () => {
|
|
1845
|
+
// Ctrl+R mid-turn: the fresh renderer is idle while Pi still streams.
|
|
1846
|
+
workspaceListResult = [WORKSPACE_TWO]
|
|
1847
|
+
activeWorkspaceResult = WORKSPACE_TWO
|
|
1848
|
+
useAppStore.setState({ activeWorkspace: WORKSPACE_TWO, workspaces: [WORKSPACE_TWO] })
|
|
1849
|
+
|
|
1850
|
+
useAppStore.getState().handleWorkspaceActivity({ [WORKSPACE_ID]: { state: 'working', since: 1 } })
|
|
1851
|
+
|
|
1852
|
+
const state = useAppStore.getState()
|
|
1853
|
+
assert.equal(state.isStreaming, true)
|
|
1854
|
+
assert.equal(state.reattachedMidTurn, true)
|
|
1855
|
+
})
|
|
1856
|
+
|
|
1857
|
+
test('the boot arm stays out of session-change teardown windows', async () => {
|
|
1858
|
+
workspaceListResult = [WORKSPACE_TWO]
|
|
1859
|
+
activeWorkspaceResult = WORKSPACE_TWO
|
|
1860
|
+
useAppStore.setState({
|
|
1861
|
+
activeWorkspace: WORKSPACE_TWO,
|
|
1862
|
+
workspaces: [WORKSPACE_TWO],
|
|
1863
|
+
sessionLoading: true,
|
|
1864
|
+
})
|
|
1865
|
+
|
|
1866
|
+
useAppStore.getState().handleWorkspaceActivity({ [WORKSPACE_ID]: { state: 'working', since: 1 } })
|
|
1867
|
+
|
|
1868
|
+
assert.equal(useAppStore.getState().reattachedMidTurn, false)
|
|
1869
|
+
})
|
|
1870
|
+
|
|
1871
|
+
test('session navigation does not require workspace re-attachment', async () => {
|
|
1872
|
+
enterWorkspacesWithBackgroundTurn()
|
|
1873
|
+
sessionStateResult = sessionStateWith(SESSION_PATH)
|
|
1874
|
+
await useAppStore.getState().openSessionItem(sessionItemFor(WORKSPACE_TWO))
|
|
1875
|
+
|
|
1876
|
+
assert.equal(useAppStore.getState().reattachedMidTurn, false)
|
|
1877
|
+
assert.equal(useAppStore.getState().isStreaming, false)
|
|
1878
|
+
})
|
|
1879
|
+
|
|
1880
|
+
test('a live on-screen stream is never re-attached over', async () => {
|
|
1881
|
+
// Same-workspace navigation back to the streaming session: the fast path
|
|
1882
|
+
// short-circuits (session already on screen) and the live stream needs no
|
|
1883
|
+
// attach — reattachedMidTurn stays clear so no spurious backfill runs.
|
|
1884
|
+
enterWorkspacesWithBackgroundTurn()
|
|
1885
|
+
activeWorkspaceResult = WORKSPACE_TWO
|
|
1886
|
+
useAppStore.setState({
|
|
1887
|
+
activeWorkspace: WORKSPACE_TWO,
|
|
1888
|
+
isStreaming: true,
|
|
1889
|
+
sessionState: sessionStateWith(SESSION_PATH),
|
|
1890
|
+
messages: [{ id: 'm1', role: 'user', content: 'hi', timestamp: 0 }],
|
|
1891
|
+
})
|
|
1892
|
+
|
|
1893
|
+
await useAppStore.getState().openSessionItem(sessionItemFor(WORKSPACE_TWO))
|
|
1894
|
+
|
|
1895
|
+
assert.equal(useAppStore.getState().reattachedMidTurn, false)
|
|
1896
|
+
assert.equal(useAppStore.getState().isStreaming, true)
|
|
1897
|
+
})
|
|
1898
|
+
|
|
1899
|
+
test('an activity update that still shows working keeps the attach alive', async () => {
|
|
1900
|
+
enterWorkspacesWithBackgroundTurn()
|
|
1901
|
+
await useAppStore.getState().switchWorkspace(WORKSPACE_ID)
|
|
1902
|
+
const loadsBefore = calls.filter((c) => c === 'getMessages').length
|
|
1903
|
+
|
|
1904
|
+
useAppStore.getState().handleWorkspaceActivity({ [WORKSPACE_ID]: { state: 'working', since: 2 } })
|
|
1905
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
1906
|
+
|
|
1907
|
+
const state = useAppStore.getState()
|
|
1908
|
+
assert.equal(state.isStreaming, true)
|
|
1909
|
+
assert.equal(state.reattachedMidTurn, true)
|
|
1910
|
+
assert.equal(calls.filter((c) => c === 'getMessages').length, loadsBefore)
|
|
1911
|
+
})
|
|
1912
|
+
|
|
1913
|
+
// ─── Deleting the session on screen ──────────────────────────────────────────
|
|
1914
|
+
// SESSION_DELETE closes the deleted session's runtime before unlinking the
|
|
1915
|
+
// file. That close promotes a sibling runtime in the same workspace and
|
|
1916
|
+
// broadcasts it as active, so the renderer has to follow the promotion.
|
|
1917
|
+
|
|
1918
|
+
function activeWorkspaceShowing(sessionFile: string): void {
|
|
1919
|
+
workspaceListResult = [WORKSPACE_ONE]
|
|
1920
|
+
activeWorkspaceResult = WORKSPACE_ONE
|
|
1921
|
+
useAppStore.setState({
|
|
1922
|
+
activeWorkspace: WORKSPACE_ONE,
|
|
1923
|
+
workspaces: [WORKSPACE_ONE],
|
|
1924
|
+
sessionState: sessionStateWith(sessionFile),
|
|
1925
|
+
})
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
test('deleting the session on screen opens the runtime main promoted', async () => {
|
|
1929
|
+
activeWorkspaceShowing(SESSION_PATH)
|
|
1930
|
+
sessionDeleteResult = { ok: true, method: 'trash', replacementSessionPath: REPLACEMENT_SESSION_PATH }
|
|
1931
|
+
|
|
1932
|
+
const result = await useAppStore.getState().deleteSession(sessionItemFor(WORKSPACE_ONE))
|
|
1933
|
+
|
|
1934
|
+
assert.equal(result.ok, true)
|
|
1935
|
+
assert.equal(
|
|
1936
|
+
calls.includes(`switch:${REPLACEMENT_SESSION_PATH}`),
|
|
1937
|
+
true,
|
|
1938
|
+
'the chat must land on the sibling main already made active'
|
|
1939
|
+
)
|
|
1940
|
+
assert.equal(
|
|
1941
|
+
calls.includes('createNew'),
|
|
1942
|
+
false,
|
|
1943
|
+
'a third runtime would steal activation from the promoted sibling'
|
|
1944
|
+
)
|
|
1945
|
+
})
|
|
1946
|
+
|
|
1947
|
+
test('deleting the last session in the workspace falls back to a new one', async () => {
|
|
1948
|
+
activeWorkspaceShowing(SESSION_PATH)
|
|
1949
|
+
sessionDeleteResult = { ok: true, method: 'trash', replacementSessionPath: null }
|
|
1950
|
+
|
|
1951
|
+
await useAppStore.getState().deleteSession(sessionItemFor(WORKSPACE_ONE))
|
|
1952
|
+
|
|
1953
|
+
assert.equal(calls.includes('createNew'), true, 'nothing is left to show, so the empty view needs a runtime')
|
|
1954
|
+
assert.equal(calls.some((c) => c.startsWith('switch:')), false)
|
|
1955
|
+
})
|
|
1956
|
+
|
|
1957
|
+
test('deleting a session that is not on screen leaves the chat alone', async () => {
|
|
1958
|
+
activeWorkspaceShowing(REPLACEMENT_SESSION_PATH)
|
|
1959
|
+
sessionDeleteResult = { ok: true, method: 'trash', replacementSessionPath: REPLACEMENT_SESSION_PATH }
|
|
1960
|
+
|
|
1961
|
+
await useAppStore.getState().deleteSession(sessionItemFor(WORKSPACE_ONE))
|
|
1962
|
+
|
|
1963
|
+
assert.equal(calls.includes(`deleteSession:${SESSION_PATH}`), true)
|
|
1964
|
+
assert.equal(calls.includes('createNew'), false, 'deleting a background session must not replace the chat')
|
|
1965
|
+
assert.equal(calls.some((c) => c.startsWith('switch:')), false)
|
|
1966
|
+
})
|
|
1967
|
+
|
|
1968
|
+
// ─── Boot/reload recovery ────────────────────────────────────────────────────
|
|
1969
|
+
|
|
1970
|
+
test('recoverPendingPrompts applies the counts snapshot and flushes the active workspace', async () => {
|
|
1971
|
+
pendingPromptsSnapshot = { 'ws-9': 2 }
|
|
1972
|
+
activeWorkspaceResult = WORKSPACE_TWO
|
|
1973
|
+
|
|
1974
|
+
await useAppStore.getState().recoverPendingPrompts()
|
|
1975
|
+
|
|
1976
|
+
assert.deepEqual(useAppStore.getState().pendingPromptCounts, { 'ws-9': 2 })
|
|
1977
|
+
assert.equal(
|
|
1978
|
+
calls.includes(`flushPendingPrompts:${WORKSPACE_TWO.id}`),
|
|
1979
|
+
true,
|
|
1980
|
+
'a reload leaves the dialog slot empty while main still holds the prompt'
|
|
1981
|
+
)
|
|
1982
|
+
})
|
|
1983
|
+
|
|
1984
|
+
test('recoverPendingPrompts applies the workspace-activity snapshot', async () => {
|
|
1985
|
+
workspaceActivitySnapshot = { 'ws-9': { state: 'completed', since: 123 } }
|
|
1986
|
+
activeWorkspaceResult = WORKSPACE_TWO
|
|
1987
|
+
|
|
1988
|
+
await useAppStore.getState().recoverPendingPrompts()
|
|
1989
|
+
|
|
1990
|
+
assert.deepEqual(useAppStore.getState().workspaceActivity, {
|
|
1991
|
+
'ws-9': { state: 'completed', since: 123 },
|
|
1992
|
+
})
|
|
1993
|
+
})
|
|
1994
|
+
|
|
1995
|
+
test('a rejected activity snapshot does not block prompt recovery', async () => {
|
|
1996
|
+
workspaceActivityFailure = 'activity bridge gone'
|
|
1997
|
+
pendingPromptsSnapshot = { 'ws-9': 2 }
|
|
1998
|
+
activeWorkspaceResult = WORKSPACE_TWO
|
|
1999
|
+
|
|
2000
|
+
await assert.doesNotReject(() => useAppStore.getState().recoverPendingPrompts())
|
|
2001
|
+
|
|
2002
|
+
assert.deepEqual(useAppStore.getState().pendingPromptCounts, { 'ws-9': 2 })
|
|
2003
|
+
assert.equal(calls.includes(`flushPendingPrompts:${WORKSPACE_TWO.id}`), true)
|
|
2004
|
+
})
|
|
2005
|
+
|
|
2006
|
+
test('recoverPendingPrompts flushes nothing when no workspace is active', async () => {
|
|
2007
|
+
pendingPromptsSnapshot = { 'ws-9': 1 }
|
|
2008
|
+
activeWorkspaceResult = null
|
|
2009
|
+
|
|
2010
|
+
await useAppStore.getState().recoverPendingPrompts()
|
|
2011
|
+
|
|
2012
|
+
assert.deepEqual(useAppStore.getState().pendingPromptCounts, { 'ws-9': 1 })
|
|
2013
|
+
assert.equal(calls.some((c) => c.startsWith('flushPendingPrompts')), false)
|
|
2014
|
+
})
|
|
2015
|
+
|
|
2016
|
+
test('recoverPendingPrompts swallows a rejected snapshot', async () => {
|
|
2017
|
+
pendingPromptsFailure = 'pending-prompts bridge gone'
|
|
2018
|
+
activeWorkspaceResult = WORKSPACE_TWO
|
|
2019
|
+
|
|
2020
|
+
await assert.doesNotReject(() => useAppStore.getState().recoverPendingPrompts())
|
|
2021
|
+
|
|
2022
|
+
assert.deepEqual(
|
|
2023
|
+
useAppStore.getState().pendingPromptCounts,
|
|
2024
|
+
{},
|
|
2025
|
+
'a failed recovery must leave the counts untouched'
|
|
2026
|
+
)
|
|
2027
|
+
assert.equal(calls.some((c) => c.startsWith('flushPendingPrompts')), false)
|
|
2028
|
+
})
|
|
2029
|
+
|
|
2030
|
+
// ─── Extension UI stacking ───────────────────────────────────────────────────
|
|
2031
|
+
|
|
2032
|
+
// A toast and a blocking dialog can be on screen together. At the same tier the
|
|
2033
|
+
// dialog's full-screen backdrop paints over the toast, so the click aimed at the
|
|
2034
|
+
// toast lands on the backdrop and cancels the dialog — a permanent tool denial.
|
|
2035
|
+
test('the notify toast sits above the blocking dialog backdrop', () => {
|
|
2036
|
+
assert.ok(
|
|
2037
|
+
NOTIFY_TOAST_Z_INDEX > DIALOG_OVERLAY_Z_INDEX,
|
|
2038
|
+
'a toast at or below the backdrop tier turns a toast click into a hard deny'
|
|
2039
|
+
)
|
|
2040
|
+
})
|
|
2041
|
+
|
|
2042
|
+
// ─── View scopes (sessions + workflow panel) ─────────────────────────────────
|
|
2043
|
+
|
|
2044
|
+
// The sessions scope is lifted into the store so SessionPanel remounts (every
|
|
2045
|
+
// navigation) can't drop it and sidebar entry points can set it explicitly.
|
|
2046
|
+
test('setSessionsScope toggles the sessions view scope', () => {
|
|
2047
|
+
assert.equal(useAppStore.getState().sessionsScope, 'all')
|
|
2048
|
+
useAppStore.getState().setSessionsScope('current')
|
|
2049
|
+
assert.equal(useAppStore.getState().sessionsScope, 'current')
|
|
2050
|
+
useAppStore.getState().setSessionsScope('all')
|
|
2051
|
+
assert.equal(useAppStore.getState().sessionsScope, 'all')
|
|
2052
|
+
})
|
|
2053
|
+
|
|
2054
|
+
test('openWorkflowRunsForWorkspace opens a project scope and clears session scope', () => {
|
|
2055
|
+
useAppStore.getState().openWorkflowRunsForWorkspace(WORKSPACE_ONE.id)
|
|
2056
|
+
const state = useAppStore.getState()
|
|
2057
|
+
assert.equal(state.workflowPanelOpen, true)
|
|
2058
|
+
assert.equal(state.workflowPanelWorkspaceId, WORKSPACE_ONE.id)
|
|
2059
|
+
assert.equal(state.workflowPanelFilter, null)
|
|
2060
|
+
})
|
|
2061
|
+
|
|
2062
|
+
test('openWorkflowRunsForWorkspace(null) opens the global scope (Tools entry)', () => {
|
|
2063
|
+
useAppStore.getState().openWorkflowRunsForWorkspace(null)
|
|
2064
|
+
const state = useAppStore.getState()
|
|
2065
|
+
assert.equal(state.workflowPanelOpen, true)
|
|
2066
|
+
assert.equal(state.workflowPanelWorkspaceId, null)
|
|
2067
|
+
assert.equal(state.workflowPanelFilter, null)
|
|
2068
|
+
})
|
|
2069
|
+
|
|
2070
|
+
test('openWorkflowRunsForSession overrides a project scope (session wins)', () => {
|
|
2071
|
+
useAppStore.getState().openWorkflowRunsForWorkspace(WORKSPACE_ONE.id)
|
|
2072
|
+
useAppStore.getState().openWorkflowRunsForSession('01b2b3c4-d5e6-4f70-a8b9-0c1d2e3f4a5b')
|
|
2073
|
+
const state = useAppStore.getState()
|
|
2074
|
+
assert.equal(state.workflowPanelWorkspaceId, null)
|
|
2075
|
+
assert.equal(state.workflowPanelFilter, '01b2b3c4-d5e6-4f70-a8b9-0c1d2e3f4a5b')
|
|
2076
|
+
})
|
|
2077
|
+
|
|
2078
|
+
test('setWorkflowPanelOpen: direct open clears scope, close preserves it', () => {
|
|
2079
|
+
useAppStore.getState().openWorkflowRunsForWorkspace(WORKSPACE_ONE.id)
|
|
2080
|
+
useAppStore.getState().setWorkflowPanelOpen(false)
|
|
2081
|
+
assert.equal(useAppStore.getState().workflowPanelOpen, false)
|
|
2082
|
+
assert.equal(
|
|
2083
|
+
useAppStore.getState().workflowPanelWorkspaceId,
|
|
2084
|
+
WORKSPACE_ONE.id,
|
|
2085
|
+
'closing must preserve the scope so a reopen stays in the same project'
|
|
2086
|
+
)
|
|
2087
|
+
useAppStore.getState().setWorkflowPanelOpen(true)
|
|
2088
|
+
assert.equal(useAppStore.getState().workflowPanelWorkspaceId, null)
|
|
2089
|
+
assert.equal(useAppStore.getState().workflowPanelFilter, null)
|
|
2090
|
+
})
|