@yancyyu/openhermit 1.6.42 → 1.6.43
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/README.md +98 -89
- package/bin/hermit.mjs +96 -0
- package/dist-renderer/assets/{ProjectEditorOverlay-DlFQ6mai.js → ProjectEditorOverlay-C98qSs7-.js} +1 -1
- package/dist-renderer/assets/{TeamGraphOverlay-D2TPMPGR.js → TeamGraphOverlay-CsBbZwcL.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-Cmd0RHLQ.js → _basePickBy-ZOyLWjMK.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-BI_iy8ea.js → _baseUniq-DBb726rt.js} +1 -1
- package/dist-renderer/assets/{arc-NzW2mjTP.js → arc-CdiTaR_R.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-Bzq85AYv.js → architectureDiagram-VXUJARFQ-Cz3sc5TH.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-D1PvYS-b.js → blockDiagram-VD42YOAC-DE4c-KJ3.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-D49RKzPC.js → c4Diagram-YG6GDRKO-CmTMDTrV.js} +1 -1
- package/dist-renderer/assets/channel-KTpqi9eT.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-fmI_MQmQ.js → chunk-4BX2VUAB-rhHy3tFl.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-Xsv9RCXZ.js → chunk-55IACEB6-fLZBzuo_.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-BE1KO8Um.js → chunk-B4BG7PRW-DOzxQhim.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-tqJ7Mv7f.js → chunk-DI55MBZ5-COQCcXC5.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-DMD45MVJ.js → chunk-FMBD7UC4-IKU9U_Y4.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-DOhGrz-q.js → chunk-QN33PNHL-D6WV154X.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-D8yDgJdD.js → chunk-QZHKN3VN-D90_2DQp.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-BcsEDu7A.js → chunk-TZMSLE5B-BQEil57G.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-lpzulY5X.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-lpzulY5X.js +1 -0
- package/dist-renderer/assets/clone-CriGymY9.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-DlSqGHMX.js → cose-bilkent-S5V4N54A-6WiK6U2P.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-BTT9tSAx.js → dagre-6UL2VRFP-DF4MMuTn.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-Du-U-mK2.js → diagram-PSM6KHXK-CcF1eZ7E.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-jFdHeKas.js → diagram-QEK2KX5R-DYlOVPQB.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-DKLNK2bu.js → diagram-S2PKOQOG-BHXWsZOP.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-CZxHgIIo.js → erDiagram-Q2GNP2WA-GjmuBx8d.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-v4XStCD0.js → flowDiagram-NV44I4VS-BuS7YVHk.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-DJjD_BEL.js → ganttDiagram-JELNMOA3-3Teu5tAa.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-BNy-jr03.js → gitGraphDiagram-V2S2FVAM-BiLdCYu5.js} +1 -1
- package/dist-renderer/assets/{graph-DDTrn6je.js → graph-CDP_R8ct.js} +1 -1
- package/dist-renderer/assets/{index-BBp78BAu.js → index-BSZdT-g-.js} +1 -1
- package/dist-renderer/assets/{index-eotrJaYy.js → index-BhWvMqsz.js} +1 -1
- package/dist-renderer/assets/{index-D8_B-cfs.js → index-C2_AupSj.js} +1 -1
- package/dist-renderer/assets/{index-BQrwHZ-k.js → index-C5ujiwAR.js} +580 -588
- package/dist-renderer/assets/index-CIS2CTK9.css +1 -0
- package/dist-renderer/assets/{index-CRKQSG9S.js → index-CVNjLwkq.js} +1 -1
- package/dist-renderer/assets/{index-DR6Wz52b.js → index-CwG3se0q.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-DqnOsuza.js → infoDiagram-HS3SLOUP-DLHUFo72.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-DTobaO1d.js → journeyDiagram-XKPGCS4Q-BE07RpJD.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-HbwVOvWc.js → kanban-definition-3W4ZIXB7-DDHZy4NB.js} +1 -1
- package/dist-renderer/assets/{layout--VYmTcw2.js → layout-5nA5wUxO.js} +1 -1
- package/dist-renderer/assets/{linear-BsJh89Mr.js → linear-BtF1i2qN.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-BZqUZePd.js → mindmap-definition-VGOIOE7T-Z1Ui9Sqy.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-B1q_nH6P.js → pieDiagram-ADFJNKIX-LCjxckWv.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-UD8QhSEu.js → quadrantDiagram-AYHSOK5B-BOwKjSco.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-BA_i7Nw8.js → requirementDiagram-UZGBJVZJ-pChP8Znd.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-CMTnX-2d.js → sankeyDiagram-TZEHDZUN-DifZ2qpo.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-BQXDB615.js → sequenceDiagram-WL72ISMW-CJg-WYyY.js} +1 -1
- package/dist-renderer/assets/{splashScene-D0YB9uxm.js → splashScene-94xWCzLA.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-BAsPXy6X.js → stateDiagram-FKZM4ZOC-DWHOoFdv.js} +1 -1
- package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-CGYZOoMb.js +1 -0
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-BdasmVkC.js → timeline-definition-IT6M3QCI-CPgokIo8.js} +1 -1
- package/dist-renderer/assets/{treemap-GDKQZRPO-BkKQqIui.js → treemap-GDKQZRPO-DAVqSR9L.js} +1 -1
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-EAlPHOdx.js → xychartDiagram-PRI3JC2R-CCOcGbrD.js} +1 -1
- package/dist-renderer/chat-community-qr.jpg +0 -0
- package/dist-renderer/fonts/Agave-Bold.ttf +0 -0
- package/dist-renderer/fonts/Agave-Regular.ttf +0 -0
- package/dist-renderer/icon.png +0 -0
- package/dist-renderer/icon.rar +0 -0
- package/dist-renderer/index.html +3 -3
- package/package.json +21 -26
- package/src/features/worker-society/core/application/WorkerSocietyService.test.ts +802 -0
- package/src/features/worker-society/core/application/WorkerSocietyService.ts +428 -0
- package/src/features/worker-society/core/application/fakes.ts +101 -0
- package/src/features/worker-society/core/application/ports.ts +70 -0
- package/src/features/worker-society/core/domain/models/society.ts +141 -0
- package/src/features/worker-society/core/domain/policies/societyPolicies.test.ts +739 -0
- package/src/features/worker-society/core/domain/policies/societyPolicies.ts +496 -0
- package/src/features/worker-society/main/adapters/input/societyMcp.test.ts +317 -0
- package/src/features/worker-society/main/adapters/input/societyMcp.ts +257 -0
- package/src/features/worker-society/main/adapters/input/societyRoutes.test.ts +695 -0
- package/src/features/worker-society/main/adapters/input/societyRoutes.ts +194 -0
- package/src/features/worker-society/main/composition/societyComposition.test.ts +74 -0
- package/src/features/worker-society/main/composition/societyComposition.ts +70 -0
- package/src/features/worker-society/main/composition/workerSocietyPlugin.test.ts +69 -0
- package/src/features/worker-society/main/composition/workerSocietyPlugin.ts +67 -0
- package/src/features/worker-society/main/infrastructure/crossTeamMessageGateway.test.ts +132 -0
- package/src/features/worker-society/main/infrastructure/crossTeamMessageGateway.ts +84 -0
- package/src/features/worker-society/main/infrastructure/fsStores.test.ts +216 -0
- package/src/features/worker-society/main/infrastructure/fsStores.ts +113 -0
- package/src/features/worker-society/main/infrastructure/mergingProfileStore.test.ts +195 -0
- package/src/features/worker-society/main/infrastructure/mergingProfileStore.ts +96 -0
- package/src/features/worker-society/renderer/SocietyGraph.tsx +166 -0
- package/src/features/worker-society/renderer/SocietyNodeLabels.tsx +139 -0
- package/src/features/worker-society/renderer/SocietyNodeOverlay.tsx +339 -0
- package/src/features/worker-society/renderer/SocietyView.tsx +437 -0
- package/src/features/worker-society/renderer/index.ts +11 -0
- package/src/features/worker-society/renderer/societyApi.test.ts +259 -0
- package/src/features/worker-society/renderer/societyApi.ts +144 -0
- package/src/features/worker-society/renderer/societyGraphAdapter.test.ts +321 -0
- package/src/features/worker-society/renderer/societyGraphAdapter.ts +240 -0
- package/src/features/worker-society/renderer/societyOverlayActions.test.ts +57 -0
- package/src/features/worker-society/renderer/societyOverlayActions.ts +49 -0
- package/src/features/worker-society/renderer/societyStore.test.ts +218 -0
- package/src/features/worker-society/renderer/societyStore.ts +146 -0
- package/src/features/worker-society/renderer/societyViewUtils.test.ts +81 -0
- package/src/features/worker-society/renderer/societyViewUtils.ts +68 -0
- package/src/main/ipc/extensions.ts +27 -0
- package/src/main/server.ts +1709 -534
- package/src/main/services/ccConnect/CcConnectBridge.ts +26 -11
- package/src/main/services/ccConnect/CcConnectClient.ts +9 -2
- package/src/main/services/ccConnect/workDirReconcile.test.ts +57 -0
- package/src/main/services/ccConnect/workDirReconcile.ts +36 -0
- package/src/main/services/direct-cli/DirectCliSessionManager.test.ts +397 -0
- package/src/main/services/direct-cli/DirectCliSessionManager.ts +508 -0
- package/src/main/services/direct-cli/DirectCliSessionStore.test.ts +79 -0
- package/src/main/services/direct-cli/DirectCliSessionStore.ts +97 -0
- package/src/main/services/direct-cli/__tests__/directCliMessageId.test.ts +40 -0
- package/src/main/services/direct-cli/directCliMessageId.ts +21 -0
- package/src/main/services/direct-cli/index.ts +17 -0
- package/src/main/services/extensions/capability-packs/CapabilityPackLoaderService.ts +637 -0
- package/src/main/services/extensions/catalog/PluginCatalogService.ts +2 -2
- package/src/main/services/loop-assets/LoopAssetsScannerService.ts +657 -0
- package/src/main/services/runtime/providerAwareCliEnv.ts +33 -5
- package/src/main/services/session-intelligence/LocalSessionScanner.ts +156 -71
- package/src/main/services/session-intelligence/SessionUsageParser.ts +103 -8
- package/src/main/services/session-intelligence/UsageTelemetryService.ts +11 -0
- package/src/main/services/session-intelligence/__tests__/teamSessionListMapper.test.ts +104 -0
- package/src/main/services/session-intelligence/teamSessionListMapper.ts +78 -0
- package/src/main/services/system-manager/AdminLoopInitializer.ts +95 -0
- package/src/main/services/system-manager/BuiltinWorkflowSeeder.ts +679 -74
- package/src/main/services/system-manager/SystemManagerConfigService.ts +19 -1
- package/src/main/services/system-manager/WorkflowPromptService.ts +58 -5
- package/src/main/services/system-manager/__tests__/AdminLoopInitializer.test.ts +129 -0
- package/src/main/services/system-manager/__tests__/SystemManagerConfigService.test.ts +60 -0
- package/src/main/services/teams-mvp/CollaborationBoardService.ts +2 -0
- package/src/main/services/teams-mvp/OpsRunbookContext.ts +60 -0
- package/src/main/services/teams-mvp/TaskDispatchService.test.ts +305 -0
- package/src/main/services/teams-mvp/TaskDispatchService.ts +250 -131
- package/src/main/services/teams-mvp/TeamProvisioningService.ts +12 -2
- package/src/main/services/teams-mvp/TeamWorkspaceService.test.ts +207 -0
- package/src/main/services/teams-mvp/TeamWorkspaceService.ts +104 -51
- package/src/main/services/teams-mvp/index.ts +6 -0
- package/src/main/utils/externalPlatformSessionRouting.ts +92 -0
- package/src/main/utils/toolApprovalRules.ts +151 -0
- package/src/renderer/App.tsx +24 -89
- package/src/renderer/api/httpClient.ts +115 -37
- package/src/renderer/api/providers.ts +5 -16
- package/src/renderer/components/chat/CommunityChatView.tsx +81 -0
- package/src/renderer/components/dashboard/DashboardView.tsx +130 -84
- package/src/renderer/components/extensions/ExtensionStoreView.tsx +39 -5
- package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +2 -1
- package/src/renderer/components/extensions/capability-packs/CapabilityPacksPanel.tsx +170 -0
- package/src/renderer/components/layout/PaneContent.tsx +10 -2
- package/src/renderer/components/layout/SortableTab.tsx +4 -0
- package/src/renderer/components/layout/TabBarActions.tsx +13 -16
- package/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx +4 -135
- package/src/renderer/components/schedules/SchedulesView.tsx +22 -14
- package/src/renderer/components/schedules/calendar/CalendarEventBlock.tsx +7 -6
- package/src/renderer/components/settings/SettingsTabs.tsx +24 -21
- package/src/renderer/components/settings/SettingsView.tsx +22 -13
- package/src/renderer/components/settings/components/SettingRow.tsx +13 -5
- package/src/renderer/components/settings/components/SettingsSectionCard.tsx +53 -0
- package/src/renderer/components/settings/components/SettingsSectionHeader.tsx +10 -6
- package/src/renderer/components/settings/components/SettingsSelect.tsx +12 -9
- package/src/renderer/components/settings/components/SettingsToggle.tsx +6 -5
- package/src/renderer/components/settings/components/index.ts +1 -0
- package/src/renderer/components/settings/sections/AdvancedSection.tsx +78 -59
- package/src/renderer/components/settings/sections/CliStatusSection.tsx +32 -44
- package/src/renderer/components/settings/sections/ConfigEditorDialog.tsx +1 -1
- package/src/renderer/components/settings/sections/GeneralSection.tsx +216 -186
- package/src/renderer/components/settings/sections/PlatformsSection.tsx +25 -17
- package/src/renderer/components/settings/sections/TaskBusSection.tsx +63 -22
- package/src/renderer/components/sidebar/SidebarSessions.tsx +120 -80
- package/src/renderer/components/sidebar/SidebarTaskItem.tsx +1 -1
- package/src/renderer/components/splash/splashScene.ts +6 -2
- package/src/renderer/components/system-manager/SystemManagerView.tsx +169 -255
- package/src/renderer/components/tasks/TasksView.tsx +63 -37
- package/src/renderer/components/team/CcSessionsSection.tsx +124 -89
- package/src/renderer/components/team/HarnessBrandLogos.tsx +318 -0
- package/src/renderer/components/team/HarnessSelect.tsx +25 -26
- package/src/renderer/components/team/TeamDetailView.tsx +137 -153
- package/src/renderer/components/team/TeamEmptyState.tsx +9 -37
- package/src/renderer/components/team/TeamListView.tsx +143 -30
- package/src/renderer/components/team/__tests__/CcSessionsSection.hasLocalFile.test.tsx +128 -0
- package/src/renderer/components/team/activity/ActivityItem.tsx +21 -9
- package/src/renderer/components/team/activity/ActivityTimeline.tsx +2 -2
- package/src/renderer/components/team/dialogs/AdvancedCliSection.tsx +1 -1
- package/src/renderer/components/team/dialogs/CreateTaskDialog.tsx +13 -10
- package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +156 -83
- package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +9 -157
- package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +19 -15
- package/src/renderer/components/team/dialogs/PlatformBindingDialog.tsx +48 -10
- package/src/renderer/components/team/dialogs/PlatformManualForm.tsx +11 -12
- package/src/renderer/components/team/dialogs/PlatformSetupQR.tsx +39 -37
- package/src/renderer/components/team/dialogs/RuntimeConfigDialog.tsx +434 -64
- package/src/renderer/components/team/dialogs/SendMessageDialog.tsx +12 -10
- package/src/renderer/components/team/dialogs/TaskDetailDialog.tsx +2 -2
- package/src/renderer/components/team/dialogs/__tests__/CreateTeamDialog.bindProject.test.tsx +399 -0
- package/src/renderer/components/team/dialogs/__tests__/CreateTeamDialog.chineseRepro.test.tsx +253 -0
- package/src/renderer/components/team/dialogs/platformAllowUtils.ts +91 -0
- package/src/renderer/components/team/dialogs/teammateRuntimeCompatibility.tsx +1 -1
- package/src/renderer/components/team/kanban/KanbanTaskCard.test.tsx +41 -0
- package/src/renderer/components/team/kanban/KanbanTaskCard.tsx +41 -86
- package/src/renderer/components/team/loop-console/LoopCommandComposer.tsx +310 -0
- package/src/renderer/components/team/loop-console/LoopConsolePanel.tsx +372 -0
- package/src/renderer/components/team/loop-console/loopSendIntent.test.ts +85 -0
- package/src/renderer/components/team/loop-console/loopSendIntent.ts +221 -0
- package/src/renderer/components/team/loop-console/useLeadSessionToolActivity.ts +74 -0
- package/src/renderer/components/team/loop-console/useLoopCommandSuggestions.ts +165 -0
- package/src/renderer/components/team/loop-console/useLoopConsoleController.ts +266 -0
- package/src/renderer/components/team/members/LeadModelRow.test.tsx +1 -1
- package/src/renderer/components/team/members/LeadModelRow.tsx +5 -3
- package/src/renderer/components/team/members/MemberDetailDialog.tsx +11 -0
- package/src/renderer/components/team/members/MemberDetailStats.tsx +13 -3
- package/src/renderer/components/team/members/MemberDraftRow.test.tsx +1 -1
- package/src/renderer/components/team/members/MemberDraftRow.tsx +1 -1
- package/src/renderer/components/team/members/MemberMessagesTab.tsx +2 -2
- package/src/renderer/components/team/members/MemberStatsTab.tsx +1 -1
- package/src/renderer/components/team/messages/MessageComposer.tsx +150 -44
- package/src/renderer/components/team/messages/MessagesFilterPopover.tsx +2 -2
- package/src/renderer/components/team/messages/MessagesPanel.tsx +34 -28
- package/src/renderer/components/team/schedule/CcCronScheduleDialog.tsx +6 -6
- package/src/renderer/components/team/taskLogs/ExactTaskLogCard.tsx +2 -2
- package/src/renderer/components/team/taskLogs/TaskLogStreamSection.tsx +1 -1
- package/src/renderer/components/terminal/TerminalPanel.tsx +2 -3
- package/src/renderer/components/ui/MentionableTextarea.tsx +5 -1
- package/src/renderer/constants/teamColors.ts +5 -5
- package/src/renderer/hooks/useExtensionsTabState.ts +1 -1
- package/src/renderer/hooks/useMentionDetection.ts +5 -1
- package/src/renderer/hooks/useProjectWorkflowCommands.ts +57 -0
- package/src/renderer/hooks/useTeamSuggestions.ts +2 -0
- package/src/renderer/index.css +19 -2
- package/src/renderer/main.tsx +7 -1
- package/src/renderer/store/index.ts +18 -1
- package/src/renderer/store/slices/extensionsSlice.ts +83 -0
- package/src/renderer/store/slices/tabSlice.ts +61 -0
- package/src/renderer/store/slices/teamSlice.ts +138 -9
- package/src/renderer/types/mention.ts +8 -0
- package/src/renderer/types/tabs.ts +3 -1
- package/src/renderer/utils/__tests__/bindProjectSlug.test.ts +69 -0
- package/src/renderer/utils/__tests__/groupTransformer.test.ts +148 -0
- package/src/renderer/utils/__tests__/initialRoute.test.ts +101 -0
- package/src/renderer/utils/__tests__/leadToolActivity.test.ts +124 -0
- package/src/renderer/utils/__tests__/mergeTeamMessages.test.ts +81 -0
- package/src/renderer/utils/__tests__/teamMessageFiltering.test.ts +213 -0
- package/src/renderer/utils/__tests__/teamMessageKey.test.ts +75 -0
- package/src/renderer/utils/__tests__/workflowCommandExecution.test.ts +173 -0
- package/src/renderer/utils/__tests__/workflowCommandSuggestions.test.ts +59 -0
- package/src/renderer/utils/bindProjectSlug.ts +57 -0
- package/src/renderer/utils/capabilityCommandExecution.ts +113 -0
- package/src/renderer/utils/initialRoute.ts +89 -0
- package/src/renderer/utils/leadToolActivity.ts +117 -0
- package/src/renderer/utils/loopShortcutSuggestions.ts +106 -0
- package/src/renderer/utils/mentionSuggestions.ts +1 -1
- package/src/renderer/utils/slashCommandRegistry.ts +231 -0
- package/src/renderer/utils/teamMentionDirective.ts +31 -0
- package/src/renderer/utils/workflowCommandExecution.ts +96 -0
- package/src/renderer/utils/workflowCommandSuggestions.ts +49 -0
- package/src/shared/types/api.ts +79 -4
- package/src/shared/types/ccConnect.ts +1 -0
- package/src/shared/types/extensions/api.ts +19 -0
- package/src/shared/types/extensions/capabilityPack.ts +118 -0
- package/src/shared/types/extensions/index.ts +29 -1
- package/src/shared/types/index.ts +6 -0
- package/src/shared/types/loopAssets.ts +54 -0
- package/src/shared/types/providers.ts +0 -16
- package/src/shared/types/systemManager.ts +26 -1
- package/src/shared/types/team.ts +41 -5
- package/src/shared/types/terminal.ts +2 -36
- package/src/shared/types/worker.test.ts +28 -0
- package/src/shared/types/worker.ts +3 -0
- package/src/shared/utils/__tests__/effortLevels.test.ts +88 -0
- package/src/shared/utils/__tests__/providerBackend.test.ts +88 -0
- package/src/shared/utils/__tests__/providerLaunchArgs.test.ts +220 -0
- package/src/shared/utils/claudeStreamJson.test.ts +187 -0
- package/src/shared/utils/claudeStreamJson.ts +153 -0
- package/src/shared/utils/providerLaunchArgs.ts +217 -0
- package/src/shared/utils/slashCommands.ts +10 -0
- package/src/types/node-pty.d.ts +8 -0
- package/dist-renderer/assets/channel-Ch7JrfUu.js +0 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-z9I4AnFy.js +0 -1
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-z9I4AnFy.js +0 -1
- package/dist-renderer/assets/clone-Dfi1Jx6l.js +0 -1
- package/dist-renderer/assets/index-iyjkpSus.css +0 -32
- package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-DTUIBfce.js +0 -1
- package/src/main/services/system-manager/SystemManagerPtyService.ts +0 -233
- package/src/renderer/components/common/TerminalPane.tsx +0 -213
- package/src/renderer/components/team/dialogs/useTeamEditForm.ts +0 -292
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
extractRecentToolActivity,
|
|
5
|
+
formatToolPreview,
|
|
6
|
+
type ToolActivityMessage,
|
|
7
|
+
} from '../leadToolActivity';
|
|
8
|
+
|
|
9
|
+
function msg(
|
|
10
|
+
timestamp: string,
|
|
11
|
+
toolCalls: Array<{ name: string; input: Record<string, unknown>; id?: string }>
|
|
12
|
+
): ToolActivityMessage {
|
|
13
|
+
return { timestamp, toolCalls };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('formatToolPreview', () => {
|
|
17
|
+
it('extracts the bash command', () => {
|
|
18
|
+
expect(formatToolPreview('Bash', { command: 'pnpm test' })).toBe('pnpm test');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('extracts file_path for read/edit/write', () => {
|
|
22
|
+
expect(formatToolPreview('Edit', { file_path: '/repo/src/index.ts' })).toBe(
|
|
23
|
+
'/repo/src/index.ts'
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('combines grep pattern and path', () => {
|
|
28
|
+
expect(formatToolPreview('Grep', { pattern: 'TODO', path: '/repo/src' })).toBe(
|
|
29
|
+
'TODO (in /repo/src)'
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('keeps pattern alone when no path', () => {
|
|
34
|
+
expect(formatToolPreview('Glob', { pattern: '**/*.ts' })).toBe('**/*.ts');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('formats Task/Agent with subagent type and description', () => {
|
|
38
|
+
expect(formatToolPreview('Task', { subagent_type: 'executor', description: 'fix bug' })).toBe(
|
|
39
|
+
'[executor] fix bug'
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('counts todos for TodoWrite', () => {
|
|
44
|
+
expect(formatToolPreview('TodoWrite', { todos: [{}, {}, {}] })).toBe('3 todos');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('falls back to the first string-valued arg for unknown tools', () => {
|
|
48
|
+
expect(formatToolPreview('CustomTool', { note: 'hello world' })).toBe('hello world');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('truncates long fallback previews', () => {
|
|
52
|
+
const long = 'x'.repeat(120);
|
|
53
|
+
expect(formatToolPreview('CustomTool', { note: long })).toBe(`${'x'.repeat(80)}…`);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns empty string when nothing useful is present', () => {
|
|
57
|
+
expect(formatToolPreview('Bash', {})).toBe('');
|
|
58
|
+
expect(formatToolPreview('Unknown', { count: 5 })).toBe('');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('extractRecentToolActivity', () => {
|
|
63
|
+
it('returns nothing for an empty session', () => {
|
|
64
|
+
expect(extractRecentToolActivity([], 5)).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('returns nothing when limit is zero or negative', () => {
|
|
68
|
+
const messages = [
|
|
69
|
+
msg('2026-06-13T00:00:00.000Z', [{ name: 'Bash', input: { command: 'ls' } }]),
|
|
70
|
+
];
|
|
71
|
+
expect(extractRecentToolActivity(messages, 0)).toEqual([]);
|
|
72
|
+
expect(extractRecentToolActivity(messages, -1)).toEqual([]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('flattens tool calls across messages and orders newest first', () => {
|
|
76
|
+
const messages = [
|
|
77
|
+
msg('2026-06-13T00:00:00.000Z', [{ name: 'Bash', input: { command: 'first' }, id: 't1' }]),
|
|
78
|
+
msg('2026-06-13T00:00:05.000Z', [
|
|
79
|
+
{ name: 'Read', input: { file_path: '/a.ts' }, id: 't2' },
|
|
80
|
+
{ name: 'Edit', input: { file_path: '/b.ts' }, id: 't3' },
|
|
81
|
+
]),
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const result = extractRecentToolActivity(messages, 10);
|
|
85
|
+
|
|
86
|
+
expect(result.map((a) => a.name)).toEqual(['Edit', 'Read', 'Bash']);
|
|
87
|
+
expect(result[0]).toEqual({
|
|
88
|
+
name: 'Edit',
|
|
89
|
+
preview: '/b.ts',
|
|
90
|
+
toolUseId: 't3',
|
|
91
|
+
timestamp: '2026-06-13T00:00:05.000Z',
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('respects the limit, keeping the most recent calls', () => {
|
|
96
|
+
const messages = [
|
|
97
|
+
msg('2026-06-13T00:00:00.000Z', [{ name: 'Bash', input: { command: 'old' }, id: 't1' }]),
|
|
98
|
+
msg('2026-06-13T00:00:01.000Z', [{ name: 'Read', input: { file_path: '/a' }, id: 't2' }]),
|
|
99
|
+
msg('2026-06-13T00:00:02.000Z', [{ name: 'Edit', input: { file_path: '/b' }, id: 't3' }]),
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const result = extractRecentToolActivity(messages, 2);
|
|
103
|
+
|
|
104
|
+
expect(result.map((a) => a.name)).toEqual(['Edit', 'Read']);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('inherits the owning message timestamp and normalizes Date objects', () => {
|
|
108
|
+
const date = new Date('2026-06-13T00:00:00.000Z');
|
|
109
|
+
const messages = [{ timestamp: date, toolCalls: [{ name: 'Bash', input: { command: 'ls' } }] }];
|
|
110
|
+
|
|
111
|
+
const [activity] = extractRecentToolActivity(messages, 5);
|
|
112
|
+
expect(activity?.timestamp).toBe('2026-06-13T00:00:00.000Z');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('skips messages that have no tool calls', () => {
|
|
116
|
+
const messages = [
|
|
117
|
+
msg('2026-06-13T00:00:00.000Z', []),
|
|
118
|
+
msg('2026-06-13T00:00:01.000Z', [{ name: 'Bash', input: { command: 'ls' }, id: 't1' }]),
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const result = extractRecentToolActivity(messages, 5);
|
|
122
|
+
expect(result.map((a) => a.name)).toEqual(['Bash']);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { mergeTeamMessages } from '../mergeTeamMessages';
|
|
4
|
+
|
|
5
|
+
import type { InboxMessage } from '@shared/types';
|
|
6
|
+
|
|
7
|
+
function makeMessage(overrides: Partial<InboxMessage> = {}): InboxMessage {
|
|
8
|
+
return {
|
|
9
|
+
from: 'user',
|
|
10
|
+
to: 'team',
|
|
11
|
+
text: 'hello',
|
|
12
|
+
timestamp: '2026-01-01T00:00:00.000Z',
|
|
13
|
+
read: true,
|
|
14
|
+
...overrides,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('mergeTeamMessages', () => {
|
|
19
|
+
it('returns empty array for no inputs', () => {
|
|
20
|
+
expect(mergeTeamMessages()).toEqual([]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('returns single list unchanged', () => {
|
|
24
|
+
const msgs = [makeMessage({ messageId: 'a' }), makeMessage({ messageId: 'b' })];
|
|
25
|
+
const result = mergeTeamMessages(msgs);
|
|
26
|
+
expect(result).toHaveLength(2);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('deduplicates by messageId', () => {
|
|
30
|
+
const listA = [makeMessage({ messageId: 'msg-1', text: 'original' })];
|
|
31
|
+
const listB = [makeMessage({ messageId: 'msg-1', text: 'updated' })];
|
|
32
|
+
const result = mergeTeamMessages(listA, listB);
|
|
33
|
+
expect(result).toHaveLength(1);
|
|
34
|
+
expect(result[0].text).toBe('updated'); // later array wins
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('deduplicates by composite key when no messageId', () => {
|
|
38
|
+
const base = { from: 'user', timestamp: '2026-06-13T01:00:00.000Z' };
|
|
39
|
+
const listA = [makeMessage({ ...base, text: 'original' })];
|
|
40
|
+
const listB = [makeMessage({ ...base, text: 'updated' })];
|
|
41
|
+
// Same from + timestamp + text prefix → same composite key
|
|
42
|
+
// BUT different text → different key, so both appear
|
|
43
|
+
// To test dedup, use same text:
|
|
44
|
+
const listC = [makeMessage({ ...base, text: 'same' })];
|
|
45
|
+
const listD = [makeMessage({ ...base, text: 'same' })];
|
|
46
|
+
const result = mergeTeamMessages(listC, listD);
|
|
47
|
+
expect(result).toHaveLength(1);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('merges messages from multiple lists, newest first', () => {
|
|
51
|
+
const listA = [makeMessage({ messageId: 'old', timestamp: '2026-01-01T00:00:00.000Z' })];
|
|
52
|
+
const listB = [makeMessage({ messageId: 'new', timestamp: '2026-06-13T01:00:00.000Z' })];
|
|
53
|
+
const result = mergeTeamMessages(listA, listB);
|
|
54
|
+
expect(result).toHaveLength(2);
|
|
55
|
+
expect(result[0].messageId).toBe('new');
|
|
56
|
+
expect(result[1].messageId).toBe('old');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('replaces optimistic message with server message by messageId', () => {
|
|
60
|
+
const optimistic = makeMessage({
|
|
61
|
+
messageId: 'optimistic-123',
|
|
62
|
+
text: '/hermit:loop-scan',
|
|
63
|
+
source: 'user_sent',
|
|
64
|
+
});
|
|
65
|
+
const server = makeMessage({
|
|
66
|
+
messageId: 'optimistic-123',
|
|
67
|
+
text: '/hermit:loop-scan',
|
|
68
|
+
source: 'runtime_delivery',
|
|
69
|
+
});
|
|
70
|
+
const result = mergeTeamMessages([optimistic], [server]);
|
|
71
|
+
expect(result).toHaveLength(1);
|
|
72
|
+
expect(result[0].source).toBe('runtime_delivery');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('keeps messages with different messageIds', () => {
|
|
76
|
+
const msgA = makeMessage({ messageId: 'msg-a', text: 'command' });
|
|
77
|
+
const msgB = makeMessage({ messageId: 'msg-b', text: 'command' });
|
|
78
|
+
const result = mergeTeamMessages([msgA], [msgB]);
|
|
79
|
+
expect(result).toHaveLength(2);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { filterTeamMessages, type TeamMessagesFilter } from '../teamMessageFiltering';
|
|
4
|
+
|
|
5
|
+
import type { InboxMessage } from '@shared/types';
|
|
6
|
+
|
|
7
|
+
const defaultFilter: TeamMessagesFilter = { from: new Set(), to: new Set(), showNoise: true };
|
|
8
|
+
|
|
9
|
+
function makeMessage(overrides: Partial<InboxMessage> = {}): InboxMessage {
|
|
10
|
+
return {
|
|
11
|
+
from: 'user',
|
|
12
|
+
to: 'team',
|
|
13
|
+
text: 'hello',
|
|
14
|
+
timestamp: '2026-01-01T00:00:00.000Z',
|
|
15
|
+
read: true,
|
|
16
|
+
...overrides,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('filterTeamMessages', () => {
|
|
21
|
+
// ── Basic filtering ──────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
it('returns all messages with empty filter', () => {
|
|
24
|
+
const msgs = [makeMessage(), makeMessage({ messageId: 'b' })];
|
|
25
|
+
const result = filterTeamMessages(msgs, { filter: defaultFilter, searchQuery: '' });
|
|
26
|
+
expect(result).toHaveLength(2);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('filters by "from" field', () => {
|
|
30
|
+
const msgs = [
|
|
31
|
+
makeMessage({ from: 'user', messageId: 'a' }),
|
|
32
|
+
makeMessage({ from: 'agent', messageId: 'b' }),
|
|
33
|
+
];
|
|
34
|
+
const result = filterTeamMessages(msgs, {
|
|
35
|
+
filter: { ...defaultFilter, from: new Set(['user']) },
|
|
36
|
+
searchQuery: '',
|
|
37
|
+
});
|
|
38
|
+
expect(result).toHaveLength(1);
|
|
39
|
+
expect(result[0].from).toBe('user');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('filters by "to" field', () => {
|
|
43
|
+
const msgs = [
|
|
44
|
+
makeMessage({ to: 'team-a', messageId: 'a' }),
|
|
45
|
+
makeMessage({ to: 'team-b', messageId: 'b' }),
|
|
46
|
+
];
|
|
47
|
+
const result = filterTeamMessages(msgs, {
|
|
48
|
+
filter: { ...defaultFilter, to: new Set(['team-a']) },
|
|
49
|
+
searchQuery: '',
|
|
50
|
+
});
|
|
51
|
+
expect(result).toHaveLength(1);
|
|
52
|
+
expect(result[0].to).toBe('team-a');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ── relayOfMessageId dedup ───────────────────────────────────
|
|
56
|
+
|
|
57
|
+
it('hides relay copy when original is visible', () => {
|
|
58
|
+
const original = makeMessage({ messageId: 'msg-1', from: 'user', to: 'lead' });
|
|
59
|
+
const relay = makeMessage({
|
|
60
|
+
messageId: 'msg-2',
|
|
61
|
+
from: 'user',
|
|
62
|
+
to: 'bridge',
|
|
63
|
+
relayOfMessageId: 'msg-1',
|
|
64
|
+
});
|
|
65
|
+
const result = filterTeamMessages([original, relay], {
|
|
66
|
+
filter: defaultFilter,
|
|
67
|
+
searchQuery: '',
|
|
68
|
+
});
|
|
69
|
+
expect(result).toHaveLength(1);
|
|
70
|
+
expect(result[0].messageId).toBe('msg-1');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('keeps relay copy when original is NOT visible (filtered out)', () => {
|
|
74
|
+
const relay = makeMessage({
|
|
75
|
+
messageId: 'msg-2',
|
|
76
|
+
from: 'other-user',
|
|
77
|
+
to: 'bridge',
|
|
78
|
+
relayOfMessageId: 'msg-1',
|
|
79
|
+
});
|
|
80
|
+
// msg-1 is not in the list at all
|
|
81
|
+
const result = filterTeamMessages([relay], { filter: defaultFilter, searchQuery: '' });
|
|
82
|
+
expect(result).toHaveLength(1);
|
|
83
|
+
expect(result[0].messageId).toBe('msg-2');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('keeps relay copy when original is filtered by "from" filter', () => {
|
|
87
|
+
const original = makeMessage({ messageId: 'msg-1', from: 'user', to: 'lead' });
|
|
88
|
+
const relay = makeMessage({
|
|
89
|
+
messageId: 'msg-2',
|
|
90
|
+
from: 'bridge',
|
|
91
|
+
to: 'lead',
|
|
92
|
+
relayOfMessageId: 'msg-1',
|
|
93
|
+
});
|
|
94
|
+
// Filter only shows messages from 'bridge', so original is hidden
|
|
95
|
+
const result = filterTeamMessages([original, relay], {
|
|
96
|
+
filter: { ...defaultFilter, from: new Set(['bridge']) },
|
|
97
|
+
searchQuery: '',
|
|
98
|
+
});
|
|
99
|
+
expect(result).toHaveLength(1);
|
|
100
|
+
expect(result[0].messageId).toBe('msg-2');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('handles message that is its own relay (relayOfMessageId === messageId)', () => {
|
|
104
|
+
const msg = makeMessage({
|
|
105
|
+
messageId: 'msg-1',
|
|
106
|
+
relayOfMessageId: 'msg-1',
|
|
107
|
+
});
|
|
108
|
+
const result = filterTeamMessages([msg], { filter: defaultFilter, searchQuery: '' });
|
|
109
|
+
expect(result).toHaveLength(1);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ── Bug: duplicate slash command messages ─────────────────────
|
|
113
|
+
|
|
114
|
+
it('BUG CASE: two messages without relayOfMessageId both show (no dedup)', () => {
|
|
115
|
+
// This is the current bug: both the direct message and the bridge copy
|
|
116
|
+
// lack relayOfMessageId, so both pass through the filter
|
|
117
|
+
const direct = makeMessage({
|
|
118
|
+
messageId: 'direct-1',
|
|
119
|
+
from: 'user',
|
|
120
|
+
to: 'hermit-dev',
|
|
121
|
+
text: '/hermit:daily-folder-hygiene',
|
|
122
|
+
});
|
|
123
|
+
const bridgeCopy = makeMessage({
|
|
124
|
+
messageId: 'bridge-1',
|
|
125
|
+
from: 'user',
|
|
126
|
+
to: 'bridge',
|
|
127
|
+
text: '/hermit:daily-folder-hygiene',
|
|
128
|
+
});
|
|
129
|
+
const result = filterTeamMessages([direct, bridgeCopy], {
|
|
130
|
+
filter: defaultFilter,
|
|
131
|
+
searchQuery: '',
|
|
132
|
+
});
|
|
133
|
+
// BUG: both appear because neither has relayOfMessageId
|
|
134
|
+
expect(result).toHaveLength(2);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('duplicate slash commands are deduped when relayOfMessageId is set correctly', () => {
|
|
138
|
+
const direct = makeMessage({
|
|
139
|
+
messageId: 'direct-1',
|
|
140
|
+
from: 'user',
|
|
141
|
+
to: 'hermit-dev',
|
|
142
|
+
text: '/hermit:daily-folder-hygiene',
|
|
143
|
+
});
|
|
144
|
+
const bridgeCopy = makeMessage({
|
|
145
|
+
messageId: 'bridge-1',
|
|
146
|
+
from: 'user',
|
|
147
|
+
to: 'bridge',
|
|
148
|
+
text: '/hermit:daily-folder-hygiene',
|
|
149
|
+
relayOfMessageId: 'direct-1', // FIX: server should set this
|
|
150
|
+
});
|
|
151
|
+
const result = filterTeamMessages([direct, bridgeCopy], {
|
|
152
|
+
filter: defaultFilter,
|
|
153
|
+
searchQuery: '',
|
|
154
|
+
});
|
|
155
|
+
expect(result).toHaveLength(1);
|
|
156
|
+
expect(result[0].messageId).toBe('direct-1');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ── Search query ─────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
it('filters by search query in text', () => {
|
|
162
|
+
const msgs = [
|
|
163
|
+
makeMessage({ text: 'hello world', messageId: 'a' }),
|
|
164
|
+
makeMessage({ text: 'foo bar', messageId: 'b' }),
|
|
165
|
+
];
|
|
166
|
+
const result = filterTeamMessages(msgs, { filter: defaultFilter, searchQuery: 'hello' });
|
|
167
|
+
expect(result).toHaveLength(1);
|
|
168
|
+
expect(result[0].messageId).toBe('a');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('filters by search query in from/to', () => {
|
|
172
|
+
const msgs = [
|
|
173
|
+
makeMessage({ from: 'alice', messageId: 'a' }),
|
|
174
|
+
makeMessage({ from: 'bob', messageId: 'b' }),
|
|
175
|
+
];
|
|
176
|
+
const result = filterTeamMessages(msgs, { filter: defaultFilter, searchQuery: 'alice' });
|
|
177
|
+
expect(result).toHaveLength(1);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// ── task_comment_notification exclusion ───────────────────────
|
|
181
|
+
|
|
182
|
+
it('excludes task_comment_notification messages', () => {
|
|
183
|
+
const msgs = [
|
|
184
|
+
makeMessage({ messageId: 'a' }),
|
|
185
|
+
makeMessage({
|
|
186
|
+
messageId: 'b',
|
|
187
|
+
messageKind: 'task_comment_notification' as InboxMessage['messageKind'],
|
|
188
|
+
}),
|
|
189
|
+
];
|
|
190
|
+
const result = filterTeamMessages(msgs, { filter: defaultFilter, searchQuery: '' });
|
|
191
|
+
expect(result).toHaveLength(1);
|
|
192
|
+
expect(result[0].messageId).toBe('a');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ── Time window ──────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
it('filters by time window', () => {
|
|
198
|
+
const msgs = [
|
|
199
|
+
makeMessage({ messageId: 'a', timestamp: '2026-01-01T00:00:00.000Z' }),
|
|
200
|
+
makeMessage({ messageId: 'b', timestamp: '2026-06-13T01:00:00.000Z' }),
|
|
201
|
+
];
|
|
202
|
+
const result = filterTeamMessages(msgs, {
|
|
203
|
+
filter: defaultFilter,
|
|
204
|
+
searchQuery: '',
|
|
205
|
+
timeWindow: {
|
|
206
|
+
start: new Date('2026-06-01').getTime(),
|
|
207
|
+
end: new Date('2026-07-01').getTime(),
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
expect(result).toHaveLength(1);
|
|
211
|
+
expect(result[0].messageId).toBe('b');
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { toMessageKey } from '../teamMessageKey';
|
|
4
|
+
|
|
5
|
+
import type { InboxMessage } from '@shared/types';
|
|
6
|
+
|
|
7
|
+
function makeMessage(overrides: Partial<InboxMessage> = {}): InboxMessage {
|
|
8
|
+
return {
|
|
9
|
+
from: 'user',
|
|
10
|
+
to: 'team',
|
|
11
|
+
text: 'hello',
|
|
12
|
+
timestamp: '2026-01-01T00:00:00.000Z',
|
|
13
|
+
read: true,
|
|
14
|
+
...overrides,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('toMessageKey', () => {
|
|
19
|
+
it('uses messageId when present', () => {
|
|
20
|
+
const msg = makeMessage({ messageId: 'msg-123' });
|
|
21
|
+
expect(toMessageKey(msg)).toBe('msg-123');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('ignores whitespace-only messageId and falls back to composite key', () => {
|
|
25
|
+
const msg = makeMessage({ messageId: ' ' });
|
|
26
|
+
const key = toMessageKey(msg);
|
|
27
|
+
expect(key).toContain('2026-01-01T00:00:00.000Z');
|
|
28
|
+
expect(key).toContain('user');
|
|
29
|
+
expect(key).toContain('hello');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('ignores empty messageId and falls back to composite key', () => {
|
|
33
|
+
const msg = makeMessage({ messageId: '' });
|
|
34
|
+
const key = toMessageKey(msg);
|
|
35
|
+
expect(key).toContain('2026-01-01T00:00:00.000Z');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('builds composite key from timestamp-from-text when no messageId', () => {
|
|
39
|
+
const msg = makeMessage({
|
|
40
|
+
from: 'agent',
|
|
41
|
+
text: 'world',
|
|
42
|
+
timestamp: '2026-06-13T01:00:00.000Z',
|
|
43
|
+
});
|
|
44
|
+
const key = toMessageKey(msg);
|
|
45
|
+
expect(key).toBe('2026-06-13T01:00:00.000Z-agent-world');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('truncates text to 80 chars in composite key', () => {
|
|
49
|
+
const longText = 'x'.repeat(200);
|
|
50
|
+
const msg = makeMessage({ text: longText });
|
|
51
|
+
const key = toMessageKey(msg);
|
|
52
|
+
// Composite key format: timestamp-from-text[:80]
|
|
53
|
+
// Key length = timestamp(24) + '-' + from(4) + '-' + text(80) = 109
|
|
54
|
+
expect(key.length).toBeLessThanOrEqual(24 + 1 + 4 + 1 + 80);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('handles missing text gracefully', () => {
|
|
58
|
+
const msg = makeMessage({ text: undefined as unknown as string });
|
|
59
|
+
const key = toMessageKey(msg);
|
|
60
|
+
expect(key).toBeDefined();
|
|
61
|
+
expect(key).toContain('2026-01-01');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('produces same key for messages with same messageId', () => {
|
|
65
|
+
const a = makeMessage({ messageId: 'shared-id', from: 'user', text: 'hello' });
|
|
66
|
+
const b = makeMessage({ messageId: 'shared-id', from: 'agent', text: 'world' });
|
|
67
|
+
expect(toMessageKey(a)).toBe(toMessageKey(b));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('produces different keys for messages without messageId but different content', () => {
|
|
71
|
+
const a = makeMessage({ from: 'user', text: 'hello' });
|
|
72
|
+
const b = makeMessage({ from: 'user', text: 'world' });
|
|
73
|
+
expect(toMessageKey(a)).not.toBe(toMessageKey(b));
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const hoisted = vi.hoisted(() => ({
|
|
4
|
+
readWorkflowPrompt: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock('@renderer/api', () => ({
|
|
8
|
+
api: {
|
|
9
|
+
systemManager: {
|
|
10
|
+
readWorkflowPrompt: hoisted.readWorkflowPrompt,
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
appendArgsToPrompt,
|
|
17
|
+
expandWorkflowCommand,
|
|
18
|
+
resolveWorkflowCommandInput,
|
|
19
|
+
} from '../workflowCommandExecution';
|
|
20
|
+
|
|
21
|
+
import type { MentionSuggestion } from '@renderer/types/mention';
|
|
22
|
+
|
|
23
|
+
function workflowSuggestion(overrides: Partial<MentionSuggestion> = {}): MentionSuggestion {
|
|
24
|
+
return {
|
|
25
|
+
id: 'admin-workflow:prompt-loop-scan',
|
|
26
|
+
name: 'loop-scan',
|
|
27
|
+
type: 'command',
|
|
28
|
+
command: '/loop-scan',
|
|
29
|
+
insertText: 'loop-scan',
|
|
30
|
+
workflowPromptId: 'prompt-loop-scan',
|
|
31
|
+
workflowPromptFolder: '/workspace/.claude/commands',
|
|
32
|
+
description: '运行 Loop Scan',
|
|
33
|
+
...overrides,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('appendArgsToPrompt', () => {
|
|
38
|
+
it('trims the prompt and returns it unchanged when there are no args', () => {
|
|
39
|
+
expect(appendArgsToPrompt(' do the thing ')).toBe('do the thing');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('appends trimmed args under a User arguments section', () => {
|
|
43
|
+
expect(appendArgsToPrompt('do the thing', ' --scope src ')).toBe(
|
|
44
|
+
'do the thing\n\nUser arguments:\n--scope src'
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('ignores whitespace-only args', () => {
|
|
49
|
+
expect(appendArgsToPrompt('do the thing', ' ')).toBe('do the thing');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('resolveWorkflowCommandInput', () => {
|
|
54
|
+
it('returns null for non-slash text', () => {
|
|
55
|
+
expect(resolveWorkflowCommandInput([workflowSuggestion()], 'just a message')).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns null when no suggestion matches the command', () => {
|
|
59
|
+
expect(resolveWorkflowCommandInput([workflowSuggestion()], '/unknown-cmd')).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('matches the typed command against a workflow suggestion and captures args', () => {
|
|
63
|
+
const resolved = resolveWorkflowCommandInput([workflowSuggestion()], '/loop-scan --scope src');
|
|
64
|
+
expect(resolved).toEqual({
|
|
65
|
+
folder: '/workspace/.claude/commands',
|
|
66
|
+
id: 'prompt-loop-scan',
|
|
67
|
+
command: '/loop-scan',
|
|
68
|
+
args: '--scope src',
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('matches case-insensitively', () => {
|
|
73
|
+
const resolved = resolveWorkflowCommandInput([workflowSuggestion()], '/Loop-Scan');
|
|
74
|
+
expect(resolved?.command).toBe('/loop-scan');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('ignores suggestions that lack workflowPromptId / workflowPromptFolder', () => {
|
|
78
|
+
const plainSuggestion = workflowSuggestion({
|
|
79
|
+
workflowPromptId: undefined,
|
|
80
|
+
workflowPromptFolder: undefined,
|
|
81
|
+
});
|
|
82
|
+
expect(resolveWorkflowCommandInput([plainSuggestion], '/loop-scan')).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('expandWorkflowCommand', () => {
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
vi.clearAllMocks();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('loads prompt content via readWorkflowPrompt and formats an injectable body with args', async () => {
|
|
92
|
+
hoisted.readWorkflowPrompt.mockResolvedValue({
|
|
93
|
+
prompt: {
|
|
94
|
+
id: 'prompt-loop-scan',
|
|
95
|
+
label: 'Loop Scan',
|
|
96
|
+
filename: 'loop-scan.md',
|
|
97
|
+
path: '/workspace/.claude/commands/loop-scan.md',
|
|
98
|
+
folder: '/workspace/.claude/commands',
|
|
99
|
+
sizeBytes: 1234,
|
|
100
|
+
updatedAt: '2026-06-13T00:00:00.000Z',
|
|
101
|
+
source: 'claude-command',
|
|
102
|
+
commandName: '/loop-scan',
|
|
103
|
+
description: 'Run loop scan',
|
|
104
|
+
category: 'loop',
|
|
105
|
+
safety: 'read-only',
|
|
106
|
+
builtin: true,
|
|
107
|
+
order: 5,
|
|
108
|
+
},
|
|
109
|
+
content: 'You are a loop scan agent. Inspect the repo.',
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const expanded = await expandWorkflowCommand({
|
|
113
|
+
folder: '/workspace/.claude/commands',
|
|
114
|
+
id: 'prompt-loop-scan',
|
|
115
|
+
command: '/loop-scan',
|
|
116
|
+
args: '--scope src',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(hoisted.readWorkflowPrompt).toHaveBeenCalledWith(
|
|
120
|
+
'/workspace/.claude/commands',
|
|
121
|
+
'prompt-loop-scan'
|
|
122
|
+
);
|
|
123
|
+
// The raw command name must NOT be the body — the full prompt content is injected,
|
|
124
|
+
// with args appended. This is the exact behavior that was missing before.
|
|
125
|
+
expect(expanded.text).toBe(
|
|
126
|
+
'You are a loop scan agent. Inspect the repo.\n\nUser arguments:\n--scope src'
|
|
127
|
+
);
|
|
128
|
+
expect(expanded.summary).toBe('Loop Scan');
|
|
129
|
+
expect(expanded.slashCommand).toEqual({
|
|
130
|
+
name: 'loop-scan',
|
|
131
|
+
command: '/loop-scan',
|
|
132
|
+
args: '--scope src',
|
|
133
|
+
knownDescription: 'Run loop scan',
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('falls back to the resolved command name when the prompt has no commandName', async () => {
|
|
138
|
+
hoisted.readWorkflowPrompt.mockResolvedValue({
|
|
139
|
+
prompt: {
|
|
140
|
+
id: 'prompt-doctor',
|
|
141
|
+
label: 'Doctor',
|
|
142
|
+
filename: 'doctor.md',
|
|
143
|
+
path: '/workspace/.claude/commands/doctor.md',
|
|
144
|
+
folder: '/workspace/.claude/commands',
|
|
145
|
+
sizeBytes: 10,
|
|
146
|
+
updatedAt: '2026-06-13T00:00:00.000Z',
|
|
147
|
+
},
|
|
148
|
+
content: 'Run diagnostics.',
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const expanded = await expandWorkflowCommand({
|
|
152
|
+
folder: '/workspace/.claude/commands',
|
|
153
|
+
id: 'prompt-doctor',
|
|
154
|
+
command: '/doctor',
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(expanded.slashCommand.command).toBe('/doctor');
|
|
158
|
+
expect(expanded.slashCommand.name).toBe('doctor');
|
|
159
|
+
expect(expanded.slashCommand.knownDescription).toBe('Doctor');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('propagates read failures so the caller can surface them', async () => {
|
|
163
|
+
hoisted.readWorkflowPrompt.mockRejectedValue(new Error('prompt not found'));
|
|
164
|
+
|
|
165
|
+
await expect(
|
|
166
|
+
expandWorkflowCommand({
|
|
167
|
+
folder: '/workspace/.claude/commands',
|
|
168
|
+
id: 'prompt-missing',
|
|
169
|
+
command: '/missing',
|
|
170
|
+
})
|
|
171
|
+
).rejects.toThrow('prompt not found');
|
|
172
|
+
});
|
|
173
|
+
});
|