@ohmykreee/dsh-remote-control 0.0.0-stage → 1.0.2
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/AGENTS.md +556 -0
- package/INSTALLATION.md +310 -0
- package/LICENSE +21 -0
- package/README.md +195 -2
- package/client.js +3 -0
- package/components.json +20 -0
- package/cordis.patch.yml +3 -0
- package/docs/README.en.md +196 -0
- package/docs/screenshots/en/overview.png +0 -0
- package/docs/screenshots/en/question.png +0 -0
- package/docs/screenshots/zh/overview.png +0 -0
- package/docs/screenshots/zh/question.png +0 -0
- package/icon.svg +24 -0
- package/index.js +2 -0
- package/lib/account.js +1 -0
- package/lib/app-mapping.js +1 -0
- package/lib/i18n.js +1 -0
- package/lib/media-enrich.js +1 -0
- package/lib/notifications.js +1 -0
- package/lib/performance.js +1 -0
- package/lib/platform/index.js +1 -0
- package/lib/probe-adapter.js +1 -0
- package/lib/probe-daemon.js +1 -0
- package/lib/probe.js +1 -0
- package/lib/qr.js +1 -0
- package/lib/runtime.js +1 -0
- package/lib/service.js +1 -0
- package/lib/sessions.js +1 -0
- package/lib/settings.js +1 -0
- package/lib/types.js +1 -0
- package/lib/util.js +1 -0
- package/lib/weather.js +1 -0
- package/lib/ws.js +1 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +118 -4
- package/scripts/browser-shot.mjs +217 -0
- package/scripts/build-dsh.mjs +228 -0
- package/scripts/build-rust.mjs +319 -0
- package/scripts/build-web.mjs +17 -0
- package/scripts/check-npm-package.mjs +322 -0
- package/scripts/mock-preview.py +574 -0
- package/scripts/screenshot-compose.py +89 -0
- package/scripts/screenshot-device.sh +75 -0
- package/src/dsh/client/api.ts +68 -0
- package/src/dsh/client/i18n.ts +573 -0
- package/src/dsh/client/icons.ts +56 -0
- package/src/dsh/client/index.ts +95 -0
- package/src/dsh/client/logic.ts +310 -0
- package/src/dsh/client/panel.ts +483 -0
- package/src/dsh/client/sections/apps.ts +40 -0
- package/src/dsh/client/sections/balance.ts +38 -0
- package/src/dsh/client/sections/connection.ts +97 -0
- package/src/dsh/client/sections/notifications.ts +43 -0
- package/src/dsh/client/sections/pomodoro.ts +184 -0
- package/src/dsh/client/sections/preferences.ts +138 -0
- package/src/dsh/client/sections/system.ts +120 -0
- package/src/dsh/client/sections/weather.ts +276 -0
- package/src/dsh/client/styles.ts +1119 -0
- package/src/dsh/client/types.ts +366 -0
- package/src/dsh/client/ui.ts +142 -0
- package/src/dsh/index.ts +108 -0
- package/src/dsh/lib/account.ts +520 -0
- package/src/dsh/lib/app-mapping.ts +366 -0
- package/src/dsh/lib/i18n.ts +82 -0
- package/src/dsh/lib/media-enrich.ts +202 -0
- package/src/dsh/lib/notifications.ts +322 -0
- package/src/dsh/lib/performance.ts +279 -0
- package/src/dsh/lib/platform/index.ts +43 -0
- package/src/dsh/lib/probe-adapter.ts +275 -0
- package/src/dsh/lib/probe-daemon.ts +359 -0
- package/src/dsh/lib/probe.ts +395 -0
- package/src/dsh/lib/qr.ts +42 -0
- package/src/dsh/lib/runtime.ts +126 -0
- package/src/dsh/lib/service.ts +1645 -0
- package/src/dsh/lib/sessions.ts +907 -0
- package/src/dsh/lib/settings.ts +468 -0
- package/src/dsh/lib/types.ts +231 -0
- package/src/dsh/lib/util.ts +115 -0
- package/src/dsh/lib/weather.ts +532 -0
- package/src/dsh/lib/ws.ts +328 -0
- package/src/dsh/types/qrcode-generator.d.ts +16 -0
- package/src/rust/Cargo.lock +1431 -0
- package/src/rust/Cargo.toml +68 -0
- package/src/rust/src/cli.rs +233 -0
- package/src/rust/src/daemon.rs +140 -0
- package/src/rust/src/location/linux.rs +118 -0
- package/src/rust/src/location/macos.rs +26 -0
- package/src/rust/src/location/mod.rs +170 -0
- package/src/rust/src/location/windows.rs +205 -0
- package/src/rust/src/main.rs +42 -0
- package/src/rust/src/media/catalog.rs +454 -0
- package/src/rust/src/media/linux.rs +350 -0
- package/src/rust/src/media/macos/accessibility.rs +59 -0
- package/src/rust/src/media/macos.rs +366 -0
- package/src/rust/src/media/mod.rs +298 -0
- package/src/rust/src/media/vendor/apple_music.rs +90 -0
- package/src/rust/src/media/vendor/common.rs +186 -0
- package/src/rust/src/media/vendor/kugou.rs +52 -0
- package/src/rust/src/media/vendor/kuwo.rs +49 -0
- package/src/rust/src/media/vendor/mod.rs +75 -0
- package/src/rust/src/media/vendor/netease.rs +1715 -0
- package/src/rust/src/media/vendor/qishui.rs +36 -0
- package/src/rust/src/media/vendor/qqmusic.rs +33 -0
- package/src/rust/src/media/vendor/spotify.rs +46 -0
- package/src/rust/src/media/vendor/wesing.rs +39 -0
- package/src/rust/src/media/vendor/youtube_music.rs +49 -0
- package/src/rust/src/media/windows/accessibility.rs +85 -0
- package/src/rust/src/media/windows/audio.rs +116 -0
- package/src/rust/src/media/windows/private.rs +291 -0
- package/src/rust/src/media/windows.rs +775 -0
- package/src/rust/src/notifications/linux.rs +276 -0
- package/src/rust/src/notifications/macos.rs +382 -0
- package/src/rust/src/notifications/mod.rs +244 -0
- package/src/rust/src/notifications/windows.rs +565 -0
- package/src/rust/src/perf/common.rs +109 -0
- package/src/rust/src/perf/linux.rs +282 -0
- package/src/rust/src/perf/macos.rs +28 -0
- package/src/rust/src/perf/mod.rs +157 -0
- package/src/rust/src/perf/windows.rs +981 -0
- package/src/rust/src/util.rs +360 -0
- package/src/rust/src/volume/linux.rs +195 -0
- package/src/rust/src/volume/macos.rs +54 -0
- package/src/rust/src/volume/mod.rs +116 -0
- package/src/rust/src/volume/windows.rs +75 -0
- package/src/rust/tests/cli.rs +79 -0
- package/src/rust/tests/daemon.rs +107 -0
- package/src/svelte/App.svelte +385 -0
- package/src/svelte/index.html +18 -0
- package/src/svelte/lib/billing.svelte.ts +228 -0
- package/src/svelte/lib/components/BalanceCard.svelte +173 -0
- package/src/svelte/lib/components/ClockCard.svelte +183 -0
- package/src/svelte/lib/components/CountdownCard.svelte +261 -0
- package/src/svelte/lib/components/CpuMemoryCard.svelte +238 -0
- package/src/svelte/lib/components/EmptyState.svelte +34 -0
- package/src/svelte/lib/components/GpuCard.svelte +245 -0
- package/src/svelte/lib/components/HomePage.svelte +34 -0
- package/src/svelte/lib/components/MusicControls.svelte +256 -0
- package/src/svelte/lib/components/MusicCover.svelte +88 -0
- package/src/svelte/lib/components/MusicPage.svelte +116 -0
- package/src/svelte/lib/components/NotificationsCard.svelte +109 -0
- package/src/svelte/lib/components/Panel.svelte +22 -0
- package/src/svelte/lib/components/PerformancePage.svelte +55 -0
- package/src/svelte/lib/components/PomodoroCard.svelte +250 -0
- package/src/svelte/lib/components/QuestionSheet.svelte +354 -0
- package/src/svelte/lib/components/RadialGauge.svelte +99 -0
- package/src/svelte/lib/components/Rail.svelte +217 -0
- package/src/svelte/lib/components/SectionHeader.svelte +40 -0
- package/src/svelte/lib/components/SeekBar.svelte +421 -0
- package/src/svelte/lib/components/SessionList.svelte +364 -0
- package/src/svelte/lib/components/SessionStats.svelte +98 -0
- package/src/svelte/lib/components/SessionsPage.svelte +61 -0
- package/src/svelte/lib/components/SpendCard.svelte +105 -0
- package/src/svelte/lib/components/ThermalCard.svelte +249 -0
- package/src/svelte/lib/components/Toast.svelte +32 -0
- package/src/svelte/lib/components/WeatherIcon.svelte +40 -0
- package/src/svelte/lib/components/WeatherPanel.svelte +420 -0
- package/src/svelte/lib/components/ui/alert/alert-action.svelte +20 -0
- package/src/svelte/lib/components/ui/alert/alert-description.svelte +23 -0
- package/src/svelte/lib/components/ui/alert/alert-title.svelte +23 -0
- package/src/svelte/lib/components/ui/alert/alert.svelte +43 -0
- package/src/svelte/lib/components/ui/alert/index.ts +17 -0
- package/src/svelte/lib/components/ui/avatar/avatar-badge.svelte +26 -0
- package/src/svelte/lib/components/ui/avatar/avatar-fallback.svelte +20 -0
- package/src/svelte/lib/components/ui/avatar/avatar-group-count.svelte +23 -0
- package/src/svelte/lib/components/ui/avatar/avatar-group.svelte +23 -0
- package/src/svelte/lib/components/ui/avatar/avatar-image.svelte +17 -0
- package/src/svelte/lib/components/ui/avatar/avatar.svelte +26 -0
- package/src/svelte/lib/components/ui/avatar/index.ts +22 -0
- package/src/svelte/lib/components/ui/badge/badge.svelte +49 -0
- package/src/svelte/lib/components/ui/badge/index.ts +2 -0
- package/src/svelte/lib/components/ui/button/button.svelte +82 -0
- package/src/svelte/lib/components/ui/button/index.ts +17 -0
- package/src/svelte/lib/components/ui/card/card-action.svelte +23 -0
- package/src/svelte/lib/components/ui/card/card-content.svelte +20 -0
- package/src/svelte/lib/components/ui/card/card-description.svelte +20 -0
- package/src/svelte/lib/components/ui/card/card-footer.svelte +20 -0
- package/src/svelte/lib/components/ui/card/card-header.svelte +23 -0
- package/src/svelte/lib/components/ui/card/card-title.svelte +20 -0
- package/src/svelte/lib/components/ui/card/card.svelte +22 -0
- package/src/svelte/lib/components/ui/card/index.ts +25 -0
- package/src/svelte/lib/components/ui/dialog/dialog-close.svelte +11 -0
- package/src/svelte/lib/components/ui/dialog/dialog-content.svelte +48 -0
- package/src/svelte/lib/components/ui/dialog/dialog-description.svelte +17 -0
- package/src/svelte/lib/components/ui/dialog/dialog-footer.svelte +32 -0
- package/src/svelte/lib/components/ui/dialog/dialog-header.svelte +20 -0
- package/src/svelte/lib/components/ui/dialog/dialog-overlay.svelte +17 -0
- package/src/svelte/lib/components/ui/dialog/dialog-portal.svelte +7 -0
- package/src/svelte/lib/components/ui/dialog/dialog-title.svelte +17 -0
- package/src/svelte/lib/components/ui/dialog/dialog-trigger.svelte +11 -0
- package/src/svelte/lib/components/ui/dialog/dialog.svelte +7 -0
- package/src/svelte/lib/components/ui/dialog/index.ts +34 -0
- package/src/svelte/lib/components/ui/empty/empty-content.svelte +23 -0
- package/src/svelte/lib/components/ui/empty/empty-description.svelte +23 -0
- package/src/svelte/lib/components/ui/empty/empty-header.svelte +20 -0
- package/src/svelte/lib/components/ui/empty/empty-media.svelte +41 -0
- package/src/svelte/lib/components/ui/empty/empty-title.svelte +20 -0
- package/src/svelte/lib/components/ui/empty/empty.svelte +23 -0
- package/src/svelte/lib/components/ui/empty/index.ts +22 -0
- package/src/svelte/lib/components/ui/input/index.ts +7 -0
- package/src/svelte/lib/components/ui/input/input.svelte +48 -0
- package/src/svelte/lib/components/ui/progress/index.ts +7 -0
- package/src/svelte/lib/components/ui/progress/progress.svelte +27 -0
- package/src/svelte/lib/components/ui/scroll-area/index.ts +10 -0
- package/src/svelte/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte +30 -0
- package/src/svelte/lib/components/ui/scroll-area/scroll-area.svelte +43 -0
- package/src/svelte/lib/components/ui/separator/index.ts +7 -0
- package/src/svelte/lib/components/ui/separator/separator.svelte +21 -0
- package/src/svelte/lib/components/ui/sheet/index.ts +34 -0
- package/src/svelte/lib/components/ui/sheet/sheet-close.svelte +7 -0
- package/src/svelte/lib/components/ui/sheet/sheet-content.svelte +55 -0
- package/src/svelte/lib/components/ui/sheet/sheet-description.svelte +17 -0
- package/src/svelte/lib/components/ui/sheet/sheet-footer.svelte +20 -0
- package/src/svelte/lib/components/ui/sheet/sheet-header.svelte +20 -0
- package/src/svelte/lib/components/ui/sheet/sheet-overlay.svelte +20 -0
- package/src/svelte/lib/components/ui/sheet/sheet-portal.svelte +7 -0
- package/src/svelte/lib/components/ui/sheet/sheet-title.svelte +17 -0
- package/src/svelte/lib/components/ui/sheet/sheet-trigger.svelte +7 -0
- package/src/svelte/lib/components/ui/sheet/sheet.svelte +7 -0
- package/src/svelte/lib/components/ui/skeleton/index.ts +7 -0
- package/src/svelte/lib/components/ui/skeleton/skeleton.svelte +17 -0
- package/src/svelte/lib/components/ui/slider/index.ts +7 -0
- package/src/svelte/lib/components/ui/slider/slider.svelte +52 -0
- package/src/svelte/lib/components/ui/tooltip/index.ts +19 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip-content.svelte +51 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip-portal.svelte +7 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip-provider.svelte +7 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip-trigger.svelte +7 -0
- package/src/svelte/lib/components/ui/tooltip/tooltip.svelte +7 -0
- package/src/svelte/lib/countdown.svelte.ts +252 -0
- package/src/svelte/lib/format.ts +199 -0
- package/src/svelte/lib/i18n.svelte.ts +583 -0
- package/src/svelte/lib/media.ts +32 -0
- package/src/svelte/lib/pages.ts +18 -0
- package/src/svelte/lib/pomodoro.svelte.ts +223 -0
- package/src/svelte/lib/session-tree.ts +80 -0
- package/src/svelte/lib/state.svelte.ts +359 -0
- package/src/svelte/lib/timer-alarm.svelte.ts +319 -0
- package/src/svelte/lib/types.ts +306 -0
- package/src/svelte/lib/utils.ts +8 -0
- package/src/svelte/lib/weather.ts +84 -0
- package/src/svelte/lib/ws.ts +104 -0
- package/src/svelte/main.ts +13 -0
- package/src/svelte/styles/global.css +315 -0
- package/src/svelte/vite-env.d.ts +12 -0
- package/static/fonts/LICENSE.txt +287 -0
- package/static/icon.svg +24 -0
- package/static/manifest.webmanifest +18 -0
- package/svelte.config.js +5 -0
- package/tsconfig.dsh-client.json +18 -0
- package/tsconfig.dsh.json +21 -0
- package/tsconfig.json +20 -0
- package/vite.config.js +31 -0
- package/web/assets/index-DLdSfnJ3.css +1 -0
- package/web/assets/index-YNE8X0Uy.js +6 -0
- package/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/web/assets/jetbrains-mono-cyrillic-ext-wght-normal-EocZY2iu.woff2 +0 -0
- package/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/web/assets/noto-sans-sc-100-wght-normal-DluiXYzi.woff2 +0 -0
- package/web/assets/noto-sans-sc-101-wght-normal-CPkZHnw0.woff2 +0 -0
- package/web/assets/noto-sans-sc-102-wght-normal-gzIulUH6.woff2 +0 -0
- package/web/assets/noto-sans-sc-103-wght-normal-CJfSBxFo.woff2 +0 -0
- package/web/assets/noto-sans-sc-104-wght-normal-B3QMglgE.woff2 +0 -0
- package/web/assets/noto-sans-sc-105-wght-normal-xXh_OMfk.woff2 +0 -0
- package/web/assets/noto-sans-sc-106-wght-normal-B9n3jMjP.woff2 +0 -0
- package/web/assets/noto-sans-sc-107-wght-normal-D7fvVprN.woff2 +0 -0
- package/web/assets/noto-sans-sc-108-wght-normal-J0_LhxVo.woff2 +0 -0
- package/web/assets/noto-sans-sc-109-wght-normal-Dsp-De_0.woff2 +0 -0
- package/web/assets/noto-sans-sc-110-wght-normal-DE_6NUrb.woff2 +0 -0
- package/web/assets/noto-sans-sc-111-wght-normal-C4S-TlTn.woff2 +0 -0
- package/web/assets/noto-sans-sc-112-wght-normal-BcFuP3w6.woff2 +0 -0
- package/web/assets/noto-sans-sc-113-wght-normal-Dk932kmj.woff2 +0 -0
- package/web/assets/noto-sans-sc-114-wght-normal-CO_5Xo-i.woff2 +0 -0
- package/web/assets/noto-sans-sc-115-wght-normal-BWy07H14.woff2 +0 -0
- package/web/assets/noto-sans-sc-116-wght-normal-B4HVAfHo.woff2 +0 -0
- package/web/assets/noto-sans-sc-117-wght-normal-siZzFqgK.woff2 +0 -0
- package/web/assets/noto-sans-sc-118-wght-normal-xhBB7f6V.woff2 +0 -0
- package/web/assets/noto-sans-sc-119-wght-normal-Ck_g2FGz.woff2 +0 -0
- package/web/assets/noto-sans-sc-21-wght-normal-D0x9-yBt.woff2 +0 -0
- package/web/assets/noto-sans-sc-22-wght-normal-BBUVTShg.woff2 +0 -0
- package/web/assets/noto-sans-sc-23-wght-normal-Du1oYWkE.woff2 +0 -0
- package/web/assets/noto-sans-sc-24-wght-normal-CmWjJ0gt.woff2 +0 -0
- package/web/assets/noto-sans-sc-25-wght-normal-BFHiqilg.woff2 +0 -0
- package/web/assets/noto-sans-sc-26-wght-normal-C0sF2Z2f.woff2 +0 -0
- package/web/assets/noto-sans-sc-27-wght-normal-COFdrquJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-28-wght-normal-DMgdNu1o.woff2 +0 -0
- package/web/assets/noto-sans-sc-29-wght-normal-Cmq8em30.woff2 +0 -0
- package/web/assets/noto-sans-sc-30-wght-normal-CZWqZ3yU.woff2 +0 -0
- package/web/assets/noto-sans-sc-31-wght-normal-Wfi_lutW.woff2 +0 -0
- package/web/assets/noto-sans-sc-32-wght-normal-BRJQ4PY5.woff2 +0 -0
- package/web/assets/noto-sans-sc-33-wght-normal-CUVQXNAJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-34-wght-normal-Bspba0wC.woff2 +0 -0
- package/web/assets/noto-sans-sc-35-wght-normal-K8fX_YPG.woff2 +0 -0
- package/web/assets/noto-sans-sc-36-wght-normal-CkITyjoT.woff2 +0 -0
- package/web/assets/noto-sans-sc-37-wght-normal-WCZ2zfEe.woff2 +0 -0
- package/web/assets/noto-sans-sc-38-wght-normal-DeM7CUce.woff2 +0 -0
- package/web/assets/noto-sans-sc-39-wght-normal-BMZNLWQU.woff2 +0 -0
- package/web/assets/noto-sans-sc-4-wght-normal-AR5bdDjz.woff2 +0 -0
- package/web/assets/noto-sans-sc-40-wght-normal-BQ5xpS5j.woff2 +0 -0
- package/web/assets/noto-sans-sc-41-wght-normal-Hthm7FFi.woff2 +0 -0
- package/web/assets/noto-sans-sc-42-wght-normal-eaS2gKds.woff2 +0 -0
- package/web/assets/noto-sans-sc-43-wght-normal-o-T53XXV.woff2 +0 -0
- package/web/assets/noto-sans-sc-44-wght-normal-BJH5J4Ez.woff2 +0 -0
- package/web/assets/noto-sans-sc-45-wght-normal-BKuxSCOJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-46-wght-normal-BR_5Mu6e.woff2 +0 -0
- package/web/assets/noto-sans-sc-47-wght-normal-CyY7gKNX.woff2 +0 -0
- package/web/assets/noto-sans-sc-48-wght-normal-BNMfTR6k.woff2 +0 -0
- package/web/assets/noto-sans-sc-49-wght-normal-DX-0y7wg.woff2 +0 -0
- package/web/assets/noto-sans-sc-5-wght-normal-CfWugllB.woff2 +0 -0
- package/web/assets/noto-sans-sc-50-wght-normal-DAOGW-Dr.woff2 +0 -0
- package/web/assets/noto-sans-sc-51-wght-normal-BlFLOjcP.woff2 +0 -0
- package/web/assets/noto-sans-sc-52-wght-normal-OZjrybnw.woff2 +0 -0
- package/web/assets/noto-sans-sc-53-wght-normal-CwyWNT6x.woff2 +0 -0
- package/web/assets/noto-sans-sc-54-wght-normal-DM0nRe18.woff2 +0 -0
- package/web/assets/noto-sans-sc-55-wght-normal-DBK2wUOR.woff2 +0 -0
- package/web/assets/noto-sans-sc-56-wght-normal-C-VKK1LS.woff2 +0 -0
- package/web/assets/noto-sans-sc-57-wght-normal-Cdbfh5kq.woff2 +0 -0
- package/web/assets/noto-sans-sc-58-wght-normal-CflMKARZ.woff2 +0 -0
- package/web/assets/noto-sans-sc-59-wght-normal-BS4Kwzxx.woff2 +0 -0
- package/web/assets/noto-sans-sc-6-wght-normal-ChnZIQfQ.woff2 +0 -0
- package/web/assets/noto-sans-sc-60-wght-normal-B7QOYsXJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-61-wght-normal-C8JjQS7w.woff2 +0 -0
- package/web/assets/noto-sans-sc-62-wght-normal-bS4wq1Gg.woff2 +0 -0
- package/web/assets/noto-sans-sc-63-wght-normal-BJr8P-OB.woff2 +0 -0
- package/web/assets/noto-sans-sc-64-wght-normal-DVB11t7j.woff2 +0 -0
- package/web/assets/noto-sans-sc-65-wght-normal-DMPCXBvK.woff2 +0 -0
- package/web/assets/noto-sans-sc-66-wght-normal-BobaG10V.woff2 +0 -0
- package/web/assets/noto-sans-sc-67-wght-normal-Dzk7WhCR.woff2 +0 -0
- package/web/assets/noto-sans-sc-68-wght-normal-DGbCD9xu.woff2 +0 -0
- package/web/assets/noto-sans-sc-69-wght-normal-Ctrv1EqA.woff2 +0 -0
- package/web/assets/noto-sans-sc-70-wght-normal-RgTO941D.woff2 +0 -0
- package/web/assets/noto-sans-sc-71-wght-normal-D9sG-CWv.woff2 +0 -0
- package/web/assets/noto-sans-sc-72-wght-normal-C4oWy3ul.woff2 +0 -0
- package/web/assets/noto-sans-sc-73-wght-normal-B1ZoOTgM.woff2 +0 -0
- package/web/assets/noto-sans-sc-74-wght-normal-F7dH_rZJ.woff2 +0 -0
- package/web/assets/noto-sans-sc-75-wght-normal-Cp2wp4si.woff2 +0 -0
- package/web/assets/noto-sans-sc-76-wght-normal-B494D7xS.woff2 +0 -0
- package/web/assets/noto-sans-sc-77-wght-normal-B3MVY9Ax.woff2 +0 -0
- package/web/assets/noto-sans-sc-78-wght-normal-5jX7iNMC.woff2 +0 -0
- package/web/assets/noto-sans-sc-79-wght-normal-BIoZy8fr.woff2 +0 -0
- package/web/assets/noto-sans-sc-80-wght-normal-Bje1Fxy1.woff2 +0 -0
- package/web/assets/noto-sans-sc-81-wght-normal-B2NN7Qi3.woff2 +0 -0
- package/web/assets/noto-sans-sc-82-wght-normal-CUggNZi4.woff2 +0 -0
- package/web/assets/noto-sans-sc-83-wght-normal-99rD8YO-.woff2 +0 -0
- package/web/assets/noto-sans-sc-84-wght-normal-zMBmUocm.woff2 +0 -0
- package/web/assets/noto-sans-sc-85-wght-normal-CjI3qdxG.woff2 +0 -0
- package/web/assets/noto-sans-sc-86-wght-normal-BHY8TVEH.woff2 +0 -0
- package/web/assets/noto-sans-sc-87-wght-normal-DioZSHnU.woff2 +0 -0
- package/web/assets/noto-sans-sc-88-wght-normal-BXYxwf2t.woff2 +0 -0
- package/web/assets/noto-sans-sc-89-wght-normal-ClMFrFaL.woff2 +0 -0
- package/web/assets/noto-sans-sc-90-wght-normal-BnOlPreP.woff2 +0 -0
- package/web/assets/noto-sans-sc-91-wght-normal-DTr9RQcS.woff2 +0 -0
- package/web/assets/noto-sans-sc-97-wght-normal-9u6Aiav-.woff2 +0 -0
- package/web/assets/noto-sans-sc-98-wght-normal-BR-H2C0V.woff2 +0 -0
- package/web/assets/noto-sans-sc-99-wght-normal-BI6XPoxp.woff2 +0 -0
- package/web/assets/noto-sans-sc-cyrillic-wght-normal-BGmsQlw-.woff2 +0 -0
- package/web/assets/noto-sans-sc-latin-ext-wght-normal-DuJTpTg_.woff2 +0 -0
- package/web/assets/noto-sans-sc-latin-wght-normal-DDW8mKAR.woff2 +0 -0
- package/web/assets/noto-sans-sc-vietnamese-wght-normal-Bi3ATJ76.woff2 +0 -0
- package/web/fonts/LICENSE.txt +287 -0
- package/web/icon.svg +24 -0
- package/web/index.html +19 -0
- package/web/manifest.webmanifest +18 -0
|
@@ -0,0 +1,907 @@
|
|
|
1
|
+
/** Live DSH sessions, their status, and pending question/approval tracking. */
|
|
2
|
+
|
|
3
|
+
import { isRecord } from './types.js';
|
|
4
|
+
import type { DshContext, DshEventHandler, JsonRecord } from './types.js';
|
|
5
|
+
import { hostMessage } from './i18n.js';
|
|
6
|
+
import type { PluginLocale } from './settings.js';
|
|
7
|
+
import { basename, finiteNumber, serializeError } from './util.js';
|
|
8
|
+
|
|
9
|
+
/** Console status of one session row, ranked in this order. */
|
|
10
|
+
export type SessionStatus = 'question' | 'running' | 'error' | 'done';
|
|
11
|
+
|
|
12
|
+
/** One option of a pending user question. */
|
|
13
|
+
export interface PublicQuestionOption {
|
|
14
|
+
label: string;
|
|
15
|
+
description: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** One pending user question with the complete selectable answer surface. */
|
|
19
|
+
export interface PublicQuestion {
|
|
20
|
+
id: string;
|
|
21
|
+
question: string;
|
|
22
|
+
header: string;
|
|
23
|
+
detail: string;
|
|
24
|
+
options: PublicQuestionOption[];
|
|
25
|
+
multiSelect: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** One answer item of a pending question batch. */
|
|
29
|
+
export interface PendingAnswerItem {
|
|
30
|
+
id: string;
|
|
31
|
+
selected: string[];
|
|
32
|
+
custom?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Complete structured answer batch, one item per question. */
|
|
36
|
+
export interface PendingAnswer {
|
|
37
|
+
answers: PendingAnswerItem[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Result of forwarding one remote answer to the waiting question. */
|
|
41
|
+
export interface PendingAnswerResult {
|
|
42
|
+
ok: boolean;
|
|
43
|
+
error?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** One pending question or approval blocking a session. */
|
|
47
|
+
export interface PendingInfo {
|
|
48
|
+
kind: 'question' | 'approval';
|
|
49
|
+
question: string;
|
|
50
|
+
header?: string;
|
|
51
|
+
detail?: string;
|
|
52
|
+
count?: number;
|
|
53
|
+
since?: number;
|
|
54
|
+
/** Stable identity the remote console echoes when it answers. */
|
|
55
|
+
requestId?: string;
|
|
56
|
+
/** Complete question batch; empty for approvals. */
|
|
57
|
+
questions?: PublicQuestion[];
|
|
58
|
+
/** Whether the remote console can submit an answer for this request. */
|
|
59
|
+
answerable?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Pending question/approval tracking shared by the overview and the session pushes. */
|
|
63
|
+
export interface PendingTracker {
|
|
64
|
+
snapshot(sessionId: string): PendingInfo[];
|
|
65
|
+
/** Forward a remote answer to the live waterfall or the continued-question service. */
|
|
66
|
+
respond(sessionId: string, requestId: string, answer: PendingAnswer): Promise<PendingAnswerResult>;
|
|
67
|
+
/** Register a question reconstructed from the session log as answerable. */
|
|
68
|
+
adopt(sessionId: string, info: PendingInfo, agent: unknown): void;
|
|
69
|
+
/** Drop continued-question handles whose tool call the log says settled. */
|
|
70
|
+
pruneContinued(sessionId: string, settledCallIds: ReadonlySet<string>): void;
|
|
71
|
+
/** True while a just-settled question must not be re-adopted from a stale log. */
|
|
72
|
+
isSettled(sessionId: string, callId: string): boolean;
|
|
73
|
+
dispose(): void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** One row of the console session list. */
|
|
77
|
+
export interface SessionSummary {
|
|
78
|
+
id: string;
|
|
79
|
+
title: string;
|
|
80
|
+
preview: string;
|
|
81
|
+
cwd: string;
|
|
82
|
+
cwdPath: string;
|
|
83
|
+
createdAt: number;
|
|
84
|
+
lastActiveAt: number;
|
|
85
|
+
turn: number;
|
|
86
|
+
model: string;
|
|
87
|
+
status: SessionStatus;
|
|
88
|
+
agentStatus: string;
|
|
89
|
+
/** Start time of the currently open turn, read from the DSH session log. */
|
|
90
|
+
runningSince: number | null;
|
|
91
|
+
error: string;
|
|
92
|
+
pending: {
|
|
93
|
+
kind: PendingInfo['kind'];
|
|
94
|
+
question: string;
|
|
95
|
+
header: string;
|
|
96
|
+
detail: string;
|
|
97
|
+
since: number;
|
|
98
|
+
count: number;
|
|
99
|
+
requestId: string;
|
|
100
|
+
questions: PublicQuestion[];
|
|
101
|
+
answerable: boolean;
|
|
102
|
+
} | null;
|
|
103
|
+
isSubagent: boolean;
|
|
104
|
+
/** Parent session id for a subagent session (durable lineage). */
|
|
105
|
+
parentId: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** One event as returned by `session.snapshotEvents`. */
|
|
109
|
+
interface SessionEvent {
|
|
110
|
+
type?: string;
|
|
111
|
+
seq?: number | string;
|
|
112
|
+
time?: number;
|
|
113
|
+
data?: JsonRecord;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Minimal session shape read by `collectSessions`. */
|
|
117
|
+
interface SessionLike {
|
|
118
|
+
id: string | number;
|
|
119
|
+
seq?: number | string;
|
|
120
|
+
snapshotEvents?(from: number): SessionEvent[];
|
|
121
|
+
eventAt?(seq: number): SessionEvent | undefined;
|
|
122
|
+
header?: { cwd?: string; createdAt?: number; origin?: string; parentSession?: string };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Minimal `sessions` service: only the list projection is used. */
|
|
126
|
+
interface SessionService {
|
|
127
|
+
list?(): SessionLike[];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface AgentModel {
|
|
131
|
+
provider?: string;
|
|
132
|
+
model?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Minimal agent shape: id, status, and the model label. */
|
|
136
|
+
interface AgentLike {
|
|
137
|
+
id?: string;
|
|
138
|
+
status?: string;
|
|
139
|
+
model?: { current?(): AgentModel | null } | null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Minimal `agents` service. */
|
|
143
|
+
interface AgentService {
|
|
144
|
+
list?(): AgentLike[];
|
|
145
|
+
get?(id: string): AgentLike | undefined;
|
|
146
|
+
currentInitiator?(): AgentLike | undefined;
|
|
147
|
+
turnBoundaryOf?(session: SessionLike): {
|
|
148
|
+
lastTurn?: number | string;
|
|
149
|
+
openTurnStartSeq?: number | string | null;
|
|
150
|
+
} | null | undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Minimal `sessionTitle` service. */
|
|
154
|
+
interface SessionTitleService {
|
|
155
|
+
get?(session: SessionLike): { title?: string } | null | undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The part of `userQuestions` used to answer a continued question. */
|
|
159
|
+
interface UserQuestionsService {
|
|
160
|
+
answer?(agent: unknown, callId: string, answer: PendingAnswer): boolean;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Failure details of the last `turn/end`, as carried by a session event. */
|
|
164
|
+
interface TurnErrorReason extends JsonRecord {
|
|
165
|
+
kind?: string;
|
|
166
|
+
error?: { message?: string };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
type HandleMode = 'live' | 'continued' | 'unavailable';
|
|
170
|
+
|
|
171
|
+
/** One answer path for a pending question. */
|
|
172
|
+
interface QuestionHandle {
|
|
173
|
+
sessionId: string;
|
|
174
|
+
requestId: string;
|
|
175
|
+
callId: string;
|
|
176
|
+
agent: unknown;
|
|
177
|
+
questions: PublicQuestion[];
|
|
178
|
+
mode: HandleMode;
|
|
179
|
+
resolve?: (answer: PendingAnswer) => void;
|
|
180
|
+
promise?: Promise<PendingAnswer>;
|
|
181
|
+
/** Removes the matching pending row from this session's snapshot. */
|
|
182
|
+
removeInfo?: () => void;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Options for {@link createPendingTracker}. */
|
|
186
|
+
export interface PendingTrackerOptions {
|
|
187
|
+
/** Called shortly after the pending set changes, so the console can refresh. */
|
|
188
|
+
onChange?: () => void;
|
|
189
|
+
/** Language used for generated session/question copy. */
|
|
190
|
+
getLocale?: () => PluginLocale;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isTurnErrorReason(value: unknown): value is TurnErrorReason {
|
|
194
|
+
return isRecord(value);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Narrow an untrusted JSON field to a display string. */
|
|
198
|
+
function asText(value: unknown): string {
|
|
199
|
+
return typeof value === 'string' ? value : '';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function messageText(message: unknown): string {
|
|
203
|
+
if (!message) return '';
|
|
204
|
+
const record: JsonRecord = isRecord(message) ? message : {};
|
|
205
|
+
const blocks: unknown[] = Array.isArray(record.content) ? record.content : Array.isArray(message) ? message : [];
|
|
206
|
+
const parts: string[] = [];
|
|
207
|
+
for (const block of blocks) {
|
|
208
|
+
if (typeof block === 'string') parts.push(block);
|
|
209
|
+
else if (isRecord(block) && block.type === 'text' && typeof block.text === 'string') parts.push(block.text);
|
|
210
|
+
}
|
|
211
|
+
return parts.join(' ').replace(/\s+/g, ' ').trim();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Normalise one question option from either the service or tool vocabulary. */
|
|
215
|
+
function normalizeOption(raw: unknown): PublicQuestionOption | null {
|
|
216
|
+
const record: JsonRecord = isRecord(raw) ? raw : {};
|
|
217
|
+
const label = asText(record.label).trim();
|
|
218
|
+
if (!label) return null;
|
|
219
|
+
return {
|
|
220
|
+
label: label.slice(0, 300),
|
|
221
|
+
description: asText(record.description).trim().slice(0, 1000),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Normalise one question from either the service or tool vocabulary. */
|
|
226
|
+
function normalizeQuestion(raw: unknown, index: number): PublicQuestion | null {
|
|
227
|
+
if (!isRecord(raw)) return null;
|
|
228
|
+
const id = asText(raw.id).trim() || `q${index + 1}`;
|
|
229
|
+
const question = asText(raw.question).trim();
|
|
230
|
+
if (!question) return null;
|
|
231
|
+
const rawOptions: unknown[] = Array.isArray(raw.options) ? raw.options : [];
|
|
232
|
+
const options: PublicQuestionOption[] = [];
|
|
233
|
+
for (const item of rawOptions.slice(0, 30)) {
|
|
234
|
+
const option = normalizeOption(item);
|
|
235
|
+
if (option) options.push(option);
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
id: id.slice(0, 200),
|
|
239
|
+
question: question.slice(0, 3000),
|
|
240
|
+
header: asText(raw.header).trim().slice(0, 200),
|
|
241
|
+
detail: asText(raw.detail).slice(0, 8000),
|
|
242
|
+
options,
|
|
243
|
+
multiSelect: raw.multiSelect === true || raw.multi_select === true,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Read a complete question batch off an event or request payload. */
|
|
248
|
+
function parseQuestionList(raw: unknown): PublicQuestion[] {
|
|
249
|
+
const list: unknown[] = Array.isArray(raw) ? raw : [];
|
|
250
|
+
const questions: PublicQuestion[] = [];
|
|
251
|
+
for (let index = 0; index < list.length && questions.length < 20; index += 1) {
|
|
252
|
+
const question = normalizeQuestion(list[index], index);
|
|
253
|
+
if (question) questions.push(question);
|
|
254
|
+
}
|
|
255
|
+
return questions;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Read a question batch off a recorded `ask_user_question` arguments string. */
|
|
259
|
+
function parseQuestionArguments(raw: unknown): PublicQuestion[] {
|
|
260
|
+
if (typeof raw !== 'string' || raw.length === 0) return [];
|
|
261
|
+
try {
|
|
262
|
+
const parsed: unknown = JSON.parse(raw);
|
|
263
|
+
return isRecord(parsed) ? parseQuestionList(parsed.questions) : [];
|
|
264
|
+
} catch {
|
|
265
|
+
/* malformed partial tool arguments are common while streaming */
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Normalise an untrusted answer batch from the remote console. */
|
|
271
|
+
export function parsePendingAnswer(raw: unknown): PendingAnswer | null {
|
|
272
|
+
if (!isRecord(raw) || !Array.isArray(raw.answers)) return null;
|
|
273
|
+
const answers: PendingAnswerItem[] = [];
|
|
274
|
+
for (const item of raw.answers.slice(0, 20)) {
|
|
275
|
+
if (!isRecord(item)) return null;
|
|
276
|
+
const id = asText(item.id).trim();
|
|
277
|
+
if (!id) return null;
|
|
278
|
+
const selected: string[] = [];
|
|
279
|
+
if (Array.isArray(item.selected)) {
|
|
280
|
+
for (const label of item.selected.slice(0, 30)) {
|
|
281
|
+
const text = asText(label).trim();
|
|
282
|
+
if (text) selected.push(text.slice(0, 300));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const custom = asText(item.custom).trim();
|
|
286
|
+
const answer: PendingAnswerItem = { id: id.slice(0, 200), selected };
|
|
287
|
+
if (custom) answer.custom = custom.slice(0, 4000);
|
|
288
|
+
answers.push(answer);
|
|
289
|
+
}
|
|
290
|
+
return { answers };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function recentEvents(session: SessionLike, count = 160): SessionEvent[] {
|
|
294
|
+
try {
|
|
295
|
+
const seq = Number(session.seq);
|
|
296
|
+
const from = Math.max(0, seq - count);
|
|
297
|
+
return session.snapshotEvents?.(from) ?? [];
|
|
298
|
+
} catch {
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Whether a `tool/result` left the question answerable as a continued request. */
|
|
304
|
+
function isContinuedResult(event: SessionEvent): boolean {
|
|
305
|
+
const message: JsonRecord = isRecord(event.data?.message) ? (event.data?.message as JsonRecord) : {};
|
|
306
|
+
const blocks: unknown[] = Array.isArray(message.content) ? message.content : [];
|
|
307
|
+
for (const block of blocks) {
|
|
308
|
+
if (!isRecord(block) || block.type !== 'text' || typeof block.text !== 'string') continue;
|
|
309
|
+
try {
|
|
310
|
+
const parsed: unknown = JSON.parse(block.text);
|
|
311
|
+
if (isRecord(parsed) && parsed.pending === true) return true;
|
|
312
|
+
} catch {
|
|
313
|
+
/* a recorded answer is not JSON we can use here */
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const error: JsonRecord = isRecord(event.data?.error) ? (event.data?.error as JsonRecord) : {};
|
|
317
|
+
const code = asText(error.code);
|
|
318
|
+
return code === 'TOOL_OUTCOME_UNKNOWN' || code === 'tool-outcome-unknown';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
interface AnswerWaitState {
|
|
322
|
+
pending: PendingInfo | null;
|
|
323
|
+
settledCallIds: Set<string>;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Find the latest `ask_user_question` call that is still open or continued.
|
|
328
|
+
* A call with a plain tool result is settled and no longer represents a wait.
|
|
329
|
+
*/
|
|
330
|
+
function findAnswerWait(events: SessionEvent[], locale: PluginLocale = 'zh'): AnswerWaitState {
|
|
331
|
+
interface CallState {
|
|
332
|
+
event: SessionEvent;
|
|
333
|
+
result: SessionEvent | null;
|
|
334
|
+
requestId: string;
|
|
335
|
+
}
|
|
336
|
+
const calls = new Map<string, CallState>();
|
|
337
|
+
const order: string[] = [];
|
|
338
|
+
|
|
339
|
+
for (const event of events) {
|
|
340
|
+
if (event?.type === 'tool/call' && event.data?.name === 'ask_user_question') {
|
|
341
|
+
const requestId = String(event.data?.callId ?? '');
|
|
342
|
+
if (!requestId) continue;
|
|
343
|
+
calls.set(requestId, { event, result: null, requestId });
|
|
344
|
+
order.push(requestId);
|
|
345
|
+
} else if (event?.type === 'tool/result') {
|
|
346
|
+
const data: JsonRecord = isRecord(event.data) ? event.data : {};
|
|
347
|
+
const message: JsonRecord = isRecord(data.message) ? data.message : {};
|
|
348
|
+
const requestId = String(data.toolCallId ?? message.toolCallId ?? '');
|
|
349
|
+
const call = calls.get(requestId);
|
|
350
|
+
if (call) call.result = event;
|
|
351
|
+
} else if (event?.type === 'user/message') {
|
|
352
|
+
const data: JsonRecord = isRecord(event.data) ? event.data : {};
|
|
353
|
+
const source: JsonRecord = isRecord(data.source) ? data.source : {};
|
|
354
|
+
if (source.kind !== 'user-question-reply') continue;
|
|
355
|
+
const requestId = String(source.callId ?? '');
|
|
356
|
+
const call = calls.get(requestId);
|
|
357
|
+
if (call) call.result = event;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const settledCallIds = new Set<string>();
|
|
362
|
+
let pending: PendingInfo | null = null;
|
|
363
|
+
for (const requestId of order) {
|
|
364
|
+
const call = calls.get(requestId);
|
|
365
|
+
if (!call) continue;
|
|
366
|
+
const continued = call.result !== null && isContinuedResult(call.result);
|
|
367
|
+
const settled = call.result !== null && !continued;
|
|
368
|
+
if (settled) {
|
|
369
|
+
settledCallIds.add(requestId);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const questions = parseQuestionArguments(call.event.data?.arguments);
|
|
373
|
+
const first = questions[0] ?? null;
|
|
374
|
+
pending = {
|
|
375
|
+
kind: 'question',
|
|
376
|
+
question: first?.question || hostMessage(locale, 'answerWaiting'),
|
|
377
|
+
header: first?.header || '',
|
|
378
|
+
detail: first?.detail || '',
|
|
379
|
+
count: questions.length || 1,
|
|
380
|
+
since: finiteNumber(call.event.time, Date.now()) ?? Date.now(),
|
|
381
|
+
requestId,
|
|
382
|
+
questions,
|
|
383
|
+
answerable: continued,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
return { pending, settledCallIds };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function deferredAnswer(): { promise: Promise<PendingAnswer>; resolve: (answer: PendingAnswer) => void } {
|
|
390
|
+
let resolve!: (answer: PendingAnswer) => void;
|
|
391
|
+
const promise = new Promise<PendingAnswer>((done) => {
|
|
392
|
+
resolve = done;
|
|
393
|
+
});
|
|
394
|
+
return { promise, resolve };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function isTimedOutError(error: unknown): boolean {
|
|
398
|
+
return isRecord(error) && (error.code === 'ASK_TIMED_OUT' || error.name === 'UserQuestionError' && error.code === 'ASK_TIMED_OUT');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function validateAnswer(questions: PublicQuestion[], answer: PendingAnswer, locale: PluginLocale): string | null {
|
|
402
|
+
if (answer.answers.length !== questions.length) {
|
|
403
|
+
return hostMessage(locale, 'answerCountMismatch', {
|
|
404
|
+
expected: questions.length,
|
|
405
|
+
received: answer.answers.length,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
const seen = new Set<string>();
|
|
409
|
+
for (const item of answer.answers) {
|
|
410
|
+
if (!item.id || seen.has(item.id)) return hostMessage(locale, 'answerDuplicate');
|
|
411
|
+
seen.add(item.id);
|
|
412
|
+
const question = questions.find((candidate) => candidate.id === item.id);
|
|
413
|
+
if (!question) return hostMessage(locale, 'answerUnknownQuestion');
|
|
414
|
+
if (question.multiSelect !== true && item.selected.length > 1) return hostMessage(locale, 'answerSingleChoice');
|
|
415
|
+
for (const label of item.selected) {
|
|
416
|
+
if (!question.options.some((option) => option.label === label)) return hostMessage(locale, 'answerOptionGone');
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
for (const question of questions) {
|
|
420
|
+
if (!seen.has(question.id)) return hostMessage(locale, 'answerMissingQuestion');
|
|
421
|
+
}
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export function createPendingTracker(ctx: DshContext, options: PendingTrackerOptions = {}): PendingTracker {
|
|
426
|
+
const locale = (): PluginLocale => options.getLocale?.() ?? 'zh';
|
|
427
|
+
const bySession = new Map<string, Map<string, PendingInfo>>();
|
|
428
|
+
const handles = new Map<string, QuestionHandle>();
|
|
429
|
+
/** `${sessionId}\0${callId}` → time the answer was sent. */
|
|
430
|
+
const settledCalls = new Map<string, number>();
|
|
431
|
+
/** Session-level suppression for a live request that settled without a call id. */
|
|
432
|
+
const settledSessions = new Map<string, number>();
|
|
433
|
+
const SETTLED_TTL = 60_000;
|
|
434
|
+
const SESSION_SETTLED_TTL = 6_000;
|
|
435
|
+
let serial = 0;
|
|
436
|
+
let notifyTimer: ReturnType<typeof setTimeout> | null = null;
|
|
437
|
+
|
|
438
|
+
function handleKey(sessionId: string, requestId: string): string {
|
|
439
|
+
return `${sessionId}\u0000${requestId}`;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function notify(): void {
|
|
443
|
+
if (!options.onChange || notifyTimer) return;
|
|
444
|
+
notifyTimer = setTimeout(() => {
|
|
445
|
+
notifyTimer = null;
|
|
446
|
+
try {
|
|
447
|
+
options.onChange?.();
|
|
448
|
+
} catch {
|
|
449
|
+
/* pushing must never break the request path */
|
|
450
|
+
}
|
|
451
|
+
}, 40);
|
|
452
|
+
notifyTimer.unref?.();
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function pruneSettled(now: number): void {
|
|
456
|
+
for (const [key, at] of settledCalls) if (now - at > SETTLED_TTL) settledCalls.delete(key);
|
|
457
|
+
for (const [key, at] of settledSessions) if (now - at > SESSION_SETTLED_TTL) settledSessions.delete(key);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function markSettled(sessionId: string, callId: string): void {
|
|
461
|
+
const now = Date.now();
|
|
462
|
+
pruneSettled(now);
|
|
463
|
+
settledSessions.set(sessionId, now);
|
|
464
|
+
if (callId) settledCalls.set(handleKey(sessionId, callId), now);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function isSettled(sessionId: string, callId: string): boolean {
|
|
468
|
+
const now = Date.now();
|
|
469
|
+
pruneSettled(now);
|
|
470
|
+
if (!sessionId) return false;
|
|
471
|
+
if (callId && settledCalls.has(handleKey(sessionId, callId))) return true;
|
|
472
|
+
const sessionAt = settledSessions.get(sessionId);
|
|
473
|
+
return sessionAt !== undefined && now - sessionAt <= SESSION_SETTLED_TTL;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function add(sessionId: string | undefined, info: PendingInfo): (() => void) | null {
|
|
477
|
+
if (!sessionId) return null;
|
|
478
|
+
const token = `${Date.now()}-${++serial}`;
|
|
479
|
+
let bucket = bySession.get(sessionId);
|
|
480
|
+
if (!bucket) {
|
|
481
|
+
bucket = new Map<string, PendingInfo>();
|
|
482
|
+
bySession.set(sessionId, bucket);
|
|
483
|
+
}
|
|
484
|
+
bucket.set(token, info);
|
|
485
|
+
notify();
|
|
486
|
+
return () => {
|
|
487
|
+
const current = bySession.get(sessionId);
|
|
488
|
+
if (!current) return;
|
|
489
|
+
current.delete(token);
|
|
490
|
+
if (current.size === 0) bySession.delete(sessionId);
|
|
491
|
+
notify();
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function snapshot(sessionId: string): PendingInfo[] {
|
|
496
|
+
const bucket = bySession.get(sessionId);
|
|
497
|
+
if (!bucket) return [];
|
|
498
|
+
return [...bucket.values()].sort((a, b) => (b.since ?? 0) - (a.since ?? 0));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function removeHandle(key: string): QuestionHandle | null {
|
|
502
|
+
const handle = handles.get(key);
|
|
503
|
+
if (!handle) return null;
|
|
504
|
+
handles.delete(key);
|
|
505
|
+
return handle;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function adopt(sessionId: string, info: PendingInfo, agent: unknown): void {
|
|
509
|
+
const requestId = info.requestId || '';
|
|
510
|
+
if (!requestId || !sessionId) return;
|
|
511
|
+
const key = handleKey(sessionId, requestId);
|
|
512
|
+
if (handles.has(key) || isSettled(sessionId, requestId)) return;
|
|
513
|
+
handles.set(key, {
|
|
514
|
+
sessionId,
|
|
515
|
+
requestId,
|
|
516
|
+
callId: requestId,
|
|
517
|
+
agent,
|
|
518
|
+
questions: info.questions ?? [],
|
|
519
|
+
mode: info.answerable === true ? 'continued' : 'unavailable',
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function pruneContinued(sessionId: string, settledCallIds: ReadonlySet<string>): void {
|
|
524
|
+
let changed = false;
|
|
525
|
+
for (const [key, handle] of [...handles]) {
|
|
526
|
+
if (handle.sessionId !== sessionId) continue;
|
|
527
|
+
if (handle.mode === 'live') continue;
|
|
528
|
+
if (!handle.callId || !settledCallIds.has(handle.callId)) continue;
|
|
529
|
+
handles.delete(key);
|
|
530
|
+
handle.removeInfo?.();
|
|
531
|
+
changed = true;
|
|
532
|
+
}
|
|
533
|
+
if (changed) notify();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async function respond(sessionId: string, requestId: string, answer: PendingAnswer): Promise<PendingAnswerResult> {
|
|
537
|
+
const key = handleKey(sessionId, requestId);
|
|
538
|
+
const handle = handles.get(key);
|
|
539
|
+
if (!handle) return { ok: false, error: hostMessage(locale(), 'answerExpired') };
|
|
540
|
+
if (handle.mode === 'unavailable') return { ok: false, error: hostMessage(locale(), 'answerNeedsDsh') };
|
|
541
|
+
const invalid = validateAnswer(handle.questions, answer, locale());
|
|
542
|
+
if (invalid) return { ok: false, error: invalid };
|
|
543
|
+
|
|
544
|
+
if (handle.mode === 'live' && handle.resolve) {
|
|
545
|
+
const resolve = handle.resolve;
|
|
546
|
+
handle.resolve = undefined;
|
|
547
|
+
markSettled(sessionId, handle.callId);
|
|
548
|
+
resolve(answer);
|
|
549
|
+
handle.removeInfo?.();
|
|
550
|
+
removeHandle(key);
|
|
551
|
+
notify();
|
|
552
|
+
return { ok: true };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (!handle.callId) {
|
|
556
|
+
removeHandle(key);
|
|
557
|
+
handle.removeInfo?.();
|
|
558
|
+
notify();
|
|
559
|
+
return { ok: false, error: hostMessage(locale(), 'answerMissingId') };
|
|
560
|
+
}
|
|
561
|
+
const service = ctx.get?.('userQuestions') as UserQuestionsService | undefined;
|
|
562
|
+
if (!service?.answer) return { ok: false, error: hostMessage(locale(), 'answerUnsupported') };
|
|
563
|
+
try {
|
|
564
|
+
const accepted = service.answer(handle.agent, handle.callId, answer);
|
|
565
|
+
if (!accepted) {
|
|
566
|
+
markSettled(sessionId, handle.callId);
|
|
567
|
+
removeHandle(key);
|
|
568
|
+
handle.removeInfo?.();
|
|
569
|
+
notify();
|
|
570
|
+
return { ok: false, error: hostMessage(locale(), 'answerNotWaiting') };
|
|
571
|
+
}
|
|
572
|
+
markSettled(sessionId, handle.callId);
|
|
573
|
+
removeHandle(key);
|
|
574
|
+
handle.removeInfo?.();
|
|
575
|
+
notify();
|
|
576
|
+
return { ok: true };
|
|
577
|
+
} catch (error) {
|
|
578
|
+
const code = isRecord(error) ? asText(error.code) : '';
|
|
579
|
+
if (code === 'CALLER_NOT_LIVE' || code === 'DELEGATED_CALLER') {
|
|
580
|
+
markSettled(sessionId, handle.callId);
|
|
581
|
+
removeHandle(key);
|
|
582
|
+
handle.removeInfo?.();
|
|
583
|
+
notify();
|
|
584
|
+
}
|
|
585
|
+
return { ok: false, error: serializeError(error) };
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function wrapApproval(): DshEventHandler {
|
|
590
|
+
return async function pendingRequest(this: unknown, request, next) {
|
|
591
|
+
const self = this as { id?: string } | undefined;
|
|
592
|
+
const agents = ctx.get?.('agents') as AgentService | undefined;
|
|
593
|
+
const body: JsonRecord = isRecord(request) ? request : {};
|
|
594
|
+
const requestAgent: JsonRecord | null = isRecord(body.agent) ? body.agent : null;
|
|
595
|
+
const requestAgentId = requestAgent && typeof requestAgent.id === 'string' ? requestAgent.id : undefined;
|
|
596
|
+
const sessionId = requestAgentId || self?.id || agents?.currentInitiator?.()?.id || undefined;
|
|
597
|
+
const display: JsonRecord = isRecord(body.displayReason) ? body.displayReason : {};
|
|
598
|
+
const localized = locale() === 'en'
|
|
599
|
+
? asText(display.en) || asText(display.zh)
|
|
600
|
+
: asText(display.zh) || asText(display.en);
|
|
601
|
+
const info: PendingInfo = {
|
|
602
|
+
kind: 'approval',
|
|
603
|
+
question: localized || asText(body.reason) || hostMessage(locale(), 'approvalPrompt', {
|
|
604
|
+
tool: asText(body.toolName) || hostMessage(locale(), 'approvalTool'),
|
|
605
|
+
}),
|
|
606
|
+
header: asText(body.toolName),
|
|
607
|
+
detail: '',
|
|
608
|
+
count: 1,
|
|
609
|
+
since: Date.now(),
|
|
610
|
+
requestId: `approval-${Date.now()}-${++serial}`,
|
|
611
|
+
questions: [],
|
|
612
|
+
answerable: false,
|
|
613
|
+
};
|
|
614
|
+
const remove = add(sessionId, info);
|
|
615
|
+
try {
|
|
616
|
+
return await next();
|
|
617
|
+
} finally {
|
|
618
|
+
remove?.();
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function wrapQuestion(): DshEventHandler {
|
|
624
|
+
return async function pendingRequest(this: unknown, request, next) {
|
|
625
|
+
const self = this as { id?: string } | undefined;
|
|
626
|
+
const agents = ctx.get?.('agents') as AgentService | undefined;
|
|
627
|
+
const body: JsonRecord = isRecord(request) ? request : {};
|
|
628
|
+
const requestAgent: JsonRecord | null = isRecord(body.agent) ? body.agent : null;
|
|
629
|
+
const requestAgentId = requestAgent && typeof requestAgent.id === 'string' ? requestAgent.id : undefined;
|
|
630
|
+
const sessionId = requestAgentId || self?.id || agents?.currentInitiator?.()?.id || undefined;
|
|
631
|
+
const agent = requestAgent || (sessionId ? agents?.get?.(sessionId) : undefined);
|
|
632
|
+
const questions = parseQuestionList(body.questions);
|
|
633
|
+
const first = questions[0] ?? null;
|
|
634
|
+
const wait: JsonRecord = isRecord(body.wait) ? body.wait : {};
|
|
635
|
+
const callId = asText(wait.callId);
|
|
636
|
+
const requestId = callId || `question-${Date.now()}-${++serial}`;
|
|
637
|
+
const info: PendingInfo = {
|
|
638
|
+
kind: 'question',
|
|
639
|
+
question: first?.question || hostMessage(locale(), 'answerWaiting'),
|
|
640
|
+
header: first?.header || '',
|
|
641
|
+
detail: first?.detail || '',
|
|
642
|
+
count: questions.length || 1,
|
|
643
|
+
since: Date.now(),
|
|
644
|
+
requestId,
|
|
645
|
+
questions,
|
|
646
|
+
answerable: true,
|
|
647
|
+
};
|
|
648
|
+
const deferred = deferredAnswer();
|
|
649
|
+
const handle: QuestionHandle = {
|
|
650
|
+
sessionId: sessionId || '',
|
|
651
|
+
requestId,
|
|
652
|
+
callId,
|
|
653
|
+
agent,
|
|
654
|
+
questions,
|
|
655
|
+
mode: 'live',
|
|
656
|
+
resolve: deferred.resolve,
|
|
657
|
+
promise: deferred.promise,
|
|
658
|
+
};
|
|
659
|
+
if (sessionId) handle.removeInfo = add(sessionId, info) ?? undefined;
|
|
660
|
+
handles.set(handleKey(sessionId || '', requestId), handle);
|
|
661
|
+
notify();
|
|
662
|
+
|
|
663
|
+
// Forward to the normal DSH answerer as well. The remote answer and the
|
|
664
|
+
// DSH answer race; whichever settles first is returned through the
|
|
665
|
+
// standard `user-questions/request` waterfall. This keeps DSH's own
|
|
666
|
+
// question card visible (it may remain until closed there, which is
|
|
667
|
+
// expected DSH client behavior).
|
|
668
|
+
const delegated = Promise.resolve().then(() => next());
|
|
669
|
+
// The DSH answerer is abandoned when a remote answer wins; its eventual
|
|
670
|
+
// settlement must not surface as an unhandled rejection.
|
|
671
|
+
delegated.catch(() => {});
|
|
672
|
+
let settled = false;
|
|
673
|
+
try {
|
|
674
|
+
const race = await Promise.race([
|
|
675
|
+
deferred.promise.then((answer) => ({ kind: 'remote' as const, answer })),
|
|
676
|
+
delegated.then((outcome) => ({ kind: 'local' as const, outcome: outcome as unknown as PendingAnswer })),
|
|
677
|
+
]);
|
|
678
|
+
settled = true;
|
|
679
|
+
if (callId) markSettled(sessionId || '', callId);
|
|
680
|
+
return race.kind === 'remote' ? race.answer : race.outcome;
|
|
681
|
+
} catch (error) {
|
|
682
|
+
if (wait.timed === true && callId && isTimedOutError(error)) {
|
|
683
|
+
// The foreground wait became a continued question. Keep the row and
|
|
684
|
+
// let `userQuestions.answer` accept the remote answer from now on.
|
|
685
|
+
handle.mode = 'continued';
|
|
686
|
+
handle.resolve = undefined;
|
|
687
|
+
notify();
|
|
688
|
+
throw error;
|
|
689
|
+
}
|
|
690
|
+
markSettled(sessionId || '', callId);
|
|
691
|
+
handles.delete(handleKey(sessionId || '', requestId));
|
|
692
|
+
handle.removeInfo?.();
|
|
693
|
+
notify();
|
|
694
|
+
throw error;
|
|
695
|
+
} finally {
|
|
696
|
+
if (settled) {
|
|
697
|
+
markSettled(sessionId || '', callId);
|
|
698
|
+
handles.delete(handleKey(sessionId || '', requestId));
|
|
699
|
+
handle.removeInfo?.();
|
|
700
|
+
notify();
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const disposers: Array<(() => void) | void> = [];
|
|
707
|
+
if (typeof ctx.on === 'function') {
|
|
708
|
+
// Prepend: the DSH Web client forwards this waterfall to the browser. If
|
|
709
|
+
// its listener ran first, it would hold the request and this tracker would
|
|
710
|
+
// never see a live question. Running first lets us track the request and
|
|
711
|
+
// then delegate to the normal answerer through `next()`.
|
|
712
|
+
disposers.push(ctx.on('user-questions/request', wrapQuestion(), { prepend: true }));
|
|
713
|
+
disposers.push(ctx.on('approval/request', wrapApproval(), { prepend: true }));
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return {
|
|
717
|
+
snapshot,
|
|
718
|
+
respond,
|
|
719
|
+
adopt,
|
|
720
|
+
pruneContinued,
|
|
721
|
+
isSettled,
|
|
722
|
+
dispose() {
|
|
723
|
+
for (const dispose of disposers) {
|
|
724
|
+
if (typeof dispose !== 'function') continue;
|
|
725
|
+
try {
|
|
726
|
+
dispose();
|
|
727
|
+
} catch {
|
|
728
|
+
/* ignore */
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
disposers.length = 0;
|
|
732
|
+
if (notifyTimer) {
|
|
733
|
+
clearTimeout(notifyTimer);
|
|
734
|
+
notifyTimer = null;
|
|
735
|
+
}
|
|
736
|
+
bySession.clear();
|
|
737
|
+
handles.clear();
|
|
738
|
+
settledCalls.clear();
|
|
739
|
+
settledSessions.clear();
|
|
740
|
+
},
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
export function collectSessions(
|
|
745
|
+
ctx: DshContext,
|
|
746
|
+
pendingTracker?: PendingTracker | null,
|
|
747
|
+
getLocale?: () => PluginLocale,
|
|
748
|
+
): SessionSummary[] {
|
|
749
|
+
const sessionService = ctx.get?.('sessions') as SessionService | undefined;
|
|
750
|
+
const sessions = sessionService?.list?.() ?? [];
|
|
751
|
+
const agents = ctx.get?.('agents') as AgentService | undefined;
|
|
752
|
+
const locale: PluginLocale = getLocale?.() ?? 'zh';
|
|
753
|
+
const agentList = agents?.list?.() ?? [];
|
|
754
|
+
const agentById = new Map<string, AgentLike>(
|
|
755
|
+
agentList.map((agent): [string, AgentLike] => [String(agent.id), agent]),
|
|
756
|
+
);
|
|
757
|
+
const titleService = ctx.get?.('sessionTitle') as SessionTitleService | undefined;
|
|
758
|
+
const now = Date.now();
|
|
759
|
+
const result: SessionSummary[] = [];
|
|
760
|
+
|
|
761
|
+
for (const session of sessions) {
|
|
762
|
+
const id = String(session.id);
|
|
763
|
+
const agent = agentById.get(id) || agents?.get?.(id);
|
|
764
|
+
const events = recentEvents(session);
|
|
765
|
+
const lastEvent = events.length > 0 ? events[events.length - 1] : null;
|
|
766
|
+
const lastActiveAt: number = finiteNumber(lastEvent?.time, null) ?? finiteNumber(session.header?.createdAt, now) ?? now;
|
|
767
|
+
const wait = findAnswerWait(events, locale);
|
|
768
|
+
pendingTracker?.pruneContinued(id, wait.settledCallIds);
|
|
769
|
+
const tracked = pendingTracker?.snapshot(id) ?? [];
|
|
770
|
+
let fallbackPending: PendingInfo | null = null;
|
|
771
|
+
if (tracked.length === 0 && agent?.status === 'running' && wait.pending) {
|
|
772
|
+
const requestId = wait.pending.requestId || '';
|
|
773
|
+
if (!pendingTracker?.isSettled(id, requestId)) {
|
|
774
|
+
fallbackPending = wait.pending;
|
|
775
|
+
pendingTracker?.adopt(id, fallbackPending, agent);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
const pending = tracked.length > 0 ? tracked : fallbackPending ? [fallbackPending] : [];
|
|
779
|
+
|
|
780
|
+
let title = '';
|
|
781
|
+
try {
|
|
782
|
+
title = titleService?.get?.(session)?.title || '';
|
|
783
|
+
} catch {
|
|
784
|
+
/* title projection may not be ready */
|
|
785
|
+
}
|
|
786
|
+
let preview = '';
|
|
787
|
+
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
788
|
+
if (events[i]?.type === 'assistant/message') {
|
|
789
|
+
preview = messageText(events[i].data?.message);
|
|
790
|
+
if (preview) break;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (!preview) {
|
|
794
|
+
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
795
|
+
if (events[i]?.type === 'user/message') {
|
|
796
|
+
preview = messageText(events[i].data);
|
|
797
|
+
if (preview) break;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (!title) {
|
|
802
|
+
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
803
|
+
if (events[i]?.type === 'user/message') {
|
|
804
|
+
title = messageText(events[i].data);
|
|
805
|
+
if (title) break;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
const cwd = session.header?.cwd || '';
|
|
810
|
+
if (!title) title = basename(cwd) || hostMessage(locale, 'newSession');
|
|
811
|
+
|
|
812
|
+
let lastTurnStart: SessionEvent | null = null;
|
|
813
|
+
let lastTurnEnd: SessionEvent | null = null;
|
|
814
|
+
for (const event of events) {
|
|
815
|
+
if (event?.type === 'turn/start') lastTurnStart = event;
|
|
816
|
+
else if (event?.type === 'turn/end') lastTurnEnd = event;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Prefer the DSH projection's open-turn marker; fall back to the recent
|
|
820
|
+
// event window so the console can show a live elapsed time while running.
|
|
821
|
+
let runningSince: number | null = null;
|
|
822
|
+
try {
|
|
823
|
+
const boundary = agents?.turnBoundaryOf?.(session);
|
|
824
|
+
const openSeq = finiteNumber(boundary?.openTurnStartSeq, null);
|
|
825
|
+
if (openSeq !== null) {
|
|
826
|
+
runningSince = finiteNumber(session.eventAt?.(openSeq)?.time, null);
|
|
827
|
+
}
|
|
828
|
+
} catch {
|
|
829
|
+
/* boundary projection may not be available */
|
|
830
|
+
}
|
|
831
|
+
if (runningSince === null && agent?.status === 'running' && lastTurnStart) {
|
|
832
|
+
runningSince = finiteNumber(lastTurnStart.time, null);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const reason = lastTurnEnd?.data?.reason;
|
|
836
|
+
const errorReason = lastTurnEnd
|
|
837
|
+
&& isTurnErrorReason(reason)
|
|
838
|
+
&& (!lastTurnStart || Number(lastTurnEnd.seq) > Number(lastTurnStart.seq))
|
|
839
|
+
&& ['error', 'blocked'].includes(reason.kind ?? '')
|
|
840
|
+
? reason
|
|
841
|
+
: null;
|
|
842
|
+
|
|
843
|
+
let turn = 0;
|
|
844
|
+
try {
|
|
845
|
+
turn = finiteNumber(agents?.turnBoundaryOf?.(session)?.lastTurn, 0) ?? 0;
|
|
846
|
+
} catch {
|
|
847
|
+
/* turn projection may not exist in a minimal composition */
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
let model = '';
|
|
851
|
+
try {
|
|
852
|
+
const current = agent?.model?.current?.();
|
|
853
|
+
if (current?.provider && current?.model) model = `${current.provider} / ${current.model}`;
|
|
854
|
+
} catch {
|
|
855
|
+
/* optional */
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const isQuestion = pending.length > 0;
|
|
859
|
+
const isRunning = agent?.status === 'running';
|
|
860
|
+
const status: SessionStatus = isQuestion ? 'question' : isRunning ? 'running' : errorReason ? 'error' : 'done';
|
|
861
|
+
const primaryPending = pending[0] || null;
|
|
862
|
+
const errorMessage = errorReason?.kind === 'error'
|
|
863
|
+
? errorReason.error?.message || hostMessage(locale, 'turnFailed')
|
|
864
|
+
: errorReason?.kind === 'blocked'
|
|
865
|
+
? hostMessage(locale, 'turnBlocked')
|
|
866
|
+
: '';
|
|
867
|
+
|
|
868
|
+
result.push({
|
|
869
|
+
id,
|
|
870
|
+
title: String(title).slice(0, 120),
|
|
871
|
+
preview: String(preview).slice(0, 180),
|
|
872
|
+
cwd: basename(cwd) || '',
|
|
873
|
+
cwdPath: cwd,
|
|
874
|
+
createdAt: finiteNumber(session.header?.createdAt, now) ?? now,
|
|
875
|
+
lastActiveAt,
|
|
876
|
+
turn,
|
|
877
|
+
model,
|
|
878
|
+
status,
|
|
879
|
+
agentStatus: agent?.status || 'inactive',
|
|
880
|
+
runningSince,
|
|
881
|
+
error: errorMessage,
|
|
882
|
+
pending: primaryPending
|
|
883
|
+
? {
|
|
884
|
+
kind: primaryPending.kind,
|
|
885
|
+
question: String(primaryPending.question || '').slice(0, 220),
|
|
886
|
+
header: String(primaryPending.header || '').slice(0, 80),
|
|
887
|
+
detail: String(primaryPending.detail || '').slice(0, 260),
|
|
888
|
+
since: finiteNumber(primaryPending.since, now) ?? now,
|
|
889
|
+
count: finiteNumber(primaryPending.count, 1) ?? 1,
|
|
890
|
+
requestId: String(primaryPending.requestId || ''),
|
|
891
|
+
questions: primaryPending.questions ?? [],
|
|
892
|
+
answerable: primaryPending.answerable === true,
|
|
893
|
+
}
|
|
894
|
+
: null,
|
|
895
|
+
isSubagent: session.header?.origin === 'subagent',
|
|
896
|
+
parentId: session.header?.parentSession ? String(session.header.parentSession) : '',
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const rank: Record<SessionStatus, number> = { question: 0, running: 1, error: 2, done: 3 };
|
|
901
|
+
result.sort((a, b) => {
|
|
902
|
+
const byStatus = (rank[a.status] ?? 3) - (rank[b.status] ?? 3);
|
|
903
|
+
if (byStatus !== 0) return byStatus;
|
|
904
|
+
return b.lastActiveAt - a.lastActiveAt;
|
|
905
|
+
});
|
|
906
|
+
return result;
|
|
907
|
+
}
|