@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
|
@@ -71,6 +71,13 @@ export class CcConnectBridge extends EventEmitter {
|
|
|
71
71
|
this.connect();
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
reconnect(): void {
|
|
75
|
+
if (this._disposed) return;
|
|
76
|
+
this._connected = false;
|
|
77
|
+
this.cleanup();
|
|
78
|
+
this.connect();
|
|
79
|
+
}
|
|
80
|
+
|
|
74
81
|
dispose(): void {
|
|
75
82
|
this._disposed = true;
|
|
76
83
|
this.cleanup();
|
|
@@ -175,15 +182,20 @@ export class CcConnectBridge extends EventEmitter {
|
|
|
175
182
|
|
|
176
183
|
logger.info(`Connecting to cc-connect Bridge: ${this.bridgeUrl}`);
|
|
177
184
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
+
const ws = (() => {
|
|
186
|
+
try {
|
|
187
|
+
return new WebSocket(url);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
logger.warn(`Failed to create WebSocket: ${error}`);
|
|
190
|
+
this.scheduleReconnect();
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
})();
|
|
194
|
+
if (!ws) return;
|
|
195
|
+
this.ws = ws;
|
|
185
196
|
|
|
186
|
-
|
|
197
|
+
ws.addEventListener('open', () => {
|
|
198
|
+
if (this.ws !== ws) return;
|
|
187
199
|
logger.info('Bridge WebSocket connected');
|
|
188
200
|
this._connected = true;
|
|
189
201
|
|
|
@@ -198,7 +210,8 @@ export class CcConnectBridge extends EventEmitter {
|
|
|
198
210
|
this.emit('connected');
|
|
199
211
|
});
|
|
200
212
|
|
|
201
|
-
|
|
213
|
+
ws.addEventListener('message', (event: MessageEvent) => {
|
|
214
|
+
if (this.ws !== ws) return;
|
|
202
215
|
try {
|
|
203
216
|
const data = typeof event.data === 'string' ? event.data : String(event.data);
|
|
204
217
|
const msg = JSON.parse(data) as CcBridgeIncomingMessage;
|
|
@@ -208,13 +221,15 @@ export class CcConnectBridge extends EventEmitter {
|
|
|
208
221
|
}
|
|
209
222
|
});
|
|
210
223
|
|
|
211
|
-
|
|
224
|
+
ws.addEventListener('close', (event: CloseEvent) => {
|
|
212
225
|
logger.info(`Bridge WebSocket closed: ${event.code} ${event.reason}`);
|
|
226
|
+
if (this.ws !== ws) return;
|
|
213
227
|
this.handleDisconnect();
|
|
214
228
|
});
|
|
215
229
|
|
|
216
|
-
|
|
230
|
+
ws.addEventListener('error', () => {
|
|
217
231
|
logger.warn('Bridge WebSocket error');
|
|
232
|
+
if (this.ws !== ws) return;
|
|
218
233
|
this.handleDisconnect(new Error('WebSocket error'));
|
|
219
234
|
});
|
|
220
235
|
}
|
|
@@ -251,11 +251,18 @@ export class CcConnectClient {
|
|
|
251
251
|
);
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
async createSession(
|
|
254
|
+
async createSession(
|
|
255
|
+
projectName: string,
|
|
256
|
+
name?: string,
|
|
257
|
+
sessionKey?: string
|
|
258
|
+
): Promise<CcSessionDetail> {
|
|
259
|
+
const body: Record<string, string> = {};
|
|
260
|
+
if (name) body.name = name;
|
|
261
|
+
if (sessionKey) body.session_key = sessionKey;
|
|
255
262
|
return this.request<CcSessionDetail>(
|
|
256
263
|
'POST',
|
|
257
264
|
`/api/v1/projects/${encodeURIComponent(projectName)}/sessions`,
|
|
258
|
-
|
|
265
|
+
body
|
|
259
266
|
);
|
|
260
267
|
}
|
|
261
268
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CC_CONNECT_PLACEHOLDER_WORK_DIR,
|
|
5
|
+
isPlaceholderWorkDir,
|
|
6
|
+
needsWorkDirReconcile,
|
|
7
|
+
} from './workDirReconcile';
|
|
8
|
+
|
|
9
|
+
describe('needsWorkDirReconcile', () => {
|
|
10
|
+
it('reconciles when the project still carries the default template placeholder', () => {
|
|
11
|
+
// The system-manager bind project ('my-project') collides with cc-connect's
|
|
12
|
+
// default template, whose work_dir is the never-filled placeholder. The agent
|
|
13
|
+
// spawn then fails with `chdir /path/to/your/project: no such file or directory`.
|
|
14
|
+
expect(needsWorkDirReconcile(CC_CONNECT_PLACEHOLDER_WORK_DIR, '/Users/me/code/hermit')).toBe(
|
|
15
|
+
true
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('does not reconcile when the work_dir already matches the manifest', () => {
|
|
20
|
+
expect(needsWorkDirReconcile('/Users/me/code/hermit', '/Users/me/code/hermit')).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('reconciles when the work_dir drifted to a different real path', () => {
|
|
24
|
+
expect(needsWorkDirReconcile('/old/path', '/new/path')).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('does not reconcile when there is no expected work_dir to enforce', () => {
|
|
28
|
+
// Never overwrite a project's work_dir with an empty value — that would clear
|
|
29
|
+
// a valid path rather than repair a bad one.
|
|
30
|
+
expect(needsWorkDirReconcile('/Users/me/code/hermit', '')).toBe(false);
|
|
31
|
+
expect(needsWorkDirReconcile('/Users/me/code/hermit', undefined)).toBe(false);
|
|
32
|
+
expect(needsWorkDirReconcile('/Users/me/code/hermit', ' ')).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('reconciles when the project has no work_dir yet but a manifest dir exists', () => {
|
|
36
|
+
expect(needsWorkDirReconcile(undefined, '/Users/me/code/hermit')).toBe(true);
|
|
37
|
+
expect(needsWorkDirReconcile('', '/Users/me/code/hermit')).toBe(true);
|
|
38
|
+
expect(needsWorkDirReconcile(null, '/Users/me/code/hermit')).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('treats whitespace-only differences as equal after trimming', () => {
|
|
42
|
+
expect(needsWorkDirReconcile(' /Users/me/code/hermit ', '/Users/me/code/hermit')).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('isPlaceholderWorkDir', () => {
|
|
47
|
+
it('recognizes the cc-connect default template placeholder', () => {
|
|
48
|
+
expect(isPlaceholderWorkDir(CC_CONNECT_PLACEHOLDER_WORK_DIR)).toBe(true);
|
|
49
|
+
expect(isPlaceholderWorkDir(' /path/to/your/project ')).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('returns false for real or empty work_dirs', () => {
|
|
53
|
+
expect(isPlaceholderWorkDir('/Users/me/code/hermit')).toBe(false);
|
|
54
|
+
expect(isPlaceholderWorkDir('')).toBe(false);
|
|
55
|
+
expect(isPlaceholderWorkDir(undefined)).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cc-connect spawns each agent subprocess with `chdir(<project work_dir>)`. When a
|
|
3
|
+
* project's work_dir is stale — or still the default template placeholder below —
|
|
4
|
+
* every session fails at agent start with `chdir ...: no such file or directory`,
|
|
5
|
+
* which cc-connect surfaces as "❌ 错误: 启动 Agent 会话失败". The session record is
|
|
6
|
+
* created fine, so the user sees the success message AND the failure.
|
|
7
|
+
*
|
|
8
|
+
* This hit the Helm Loop hardest: its bind project is `my-project`, the same name
|
|
9
|
+
* cc-connect ships as an unconfigured template (`work_dir = /path/to/your/project`).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** cc-connect's default template project work_dir — a placeholder that was never filled. */
|
|
13
|
+
export const CC_CONNECT_PLACEHOLDER_WORK_DIR = '/path/to/your/project';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Decide whether a cc-connect project's work_dir must be reconciled to the
|
|
17
|
+
* team manifest's work_dir.
|
|
18
|
+
*
|
|
19
|
+
* Reconcile only when we have a concrete expected dir AND the project's current
|
|
20
|
+
* work_dir differs. The placeholder is caught by the inequality check, so the
|
|
21
|
+
* common broken case needs no special-casing. We never overwrite with an empty
|
|
22
|
+
* expected value — that would clear a valid path instead of repairing a bad one.
|
|
23
|
+
*/
|
|
24
|
+
export function needsWorkDirReconcile(
|
|
25
|
+
actualWorkDir: string | undefined | null,
|
|
26
|
+
expectedWorkDir: string | undefined | null
|
|
27
|
+
): boolean {
|
|
28
|
+
const expected = expectedWorkDir?.trim();
|
|
29
|
+
if (!expected) return false;
|
|
30
|
+
return (actualWorkDir?.trim() ?? '') !== expected;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** True when a work_dir is cc-connect's default template placeholder. */
|
|
34
|
+
export function isPlaceholderWorkDir(workDir: string | undefined | null): boolean {
|
|
35
|
+
return workDir?.trim() === CC_CONNECT_PLACEHOLDER_WORK_DIR;
|
|
36
|
+
}
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
|
|
3
|
+
import type { SpawnOptions } from 'child_process';
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
buildClaudeStreamArgs,
|
|
9
|
+
DirectCliSessionManager,
|
|
10
|
+
formatClaudeStdinUserTurn,
|
|
11
|
+
} from './DirectCliSessionManager';
|
|
12
|
+
|
|
13
|
+
/** Minimal fake ChildProcess: stdout/stderr/stdin as EventEmitters + kill. */
|
|
14
|
+
interface FakeChild {
|
|
15
|
+
stdout: EventEmitter;
|
|
16
|
+
stderr: EventEmitter;
|
|
17
|
+
stdin: { write: (data: string) => boolean; destroyed: boolean };
|
|
18
|
+
kill: (signal?: string) => void;
|
|
19
|
+
on: (event: string, cb: (...args: unknown[]) => void) => void;
|
|
20
|
+
emitExit: (code: number | null) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createFakeChild(): FakeChild {
|
|
24
|
+
const bus = new EventEmitter();
|
|
25
|
+
const child: FakeChild = {
|
|
26
|
+
stdout: new EventEmitter(),
|
|
27
|
+
stderr: new EventEmitter(),
|
|
28
|
+
stdin: { write: () => true, destroyed: false },
|
|
29
|
+
kill: () => undefined,
|
|
30
|
+
on: (event, cb) => bus.on(event, cb),
|
|
31
|
+
emitExit: (code) => bus.emit('exit', code),
|
|
32
|
+
};
|
|
33
|
+
return child;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createManager(providerArgs: string[] = []): {
|
|
37
|
+
manager: DirectCliSessionManager;
|
|
38
|
+
child: FakeChild;
|
|
39
|
+
} {
|
|
40
|
+
const child = createFakeChild();
|
|
41
|
+
const manager = new DirectCliSessionManager({
|
|
42
|
+
spawnFn: () => child as unknown as import('child_process').ChildProcess,
|
|
43
|
+
envResolver: async () => ({ env: { PATH: '/fake' }, providerArgs }),
|
|
44
|
+
binaryResolver: { resolve: async () => '/fake/claude' } as never,
|
|
45
|
+
store: new Map<string, string>() as never,
|
|
46
|
+
});
|
|
47
|
+
return { manager, child };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('buildClaudeStreamArgs', () => {
|
|
51
|
+
it('emits the base stream-json flags with --verbose by default', () => {
|
|
52
|
+
expect(buildClaudeStreamArgs()).toEqual([
|
|
53
|
+
'--output-format',
|
|
54
|
+
'stream-json',
|
|
55
|
+
'--input-format',
|
|
56
|
+
'stream-json',
|
|
57
|
+
'--permission-prompt-tool',
|
|
58
|
+
'stdio',
|
|
59
|
+
'--verbose',
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('adds --resume / --append-system-prompt / provider args when provided', () => {
|
|
64
|
+
expect(
|
|
65
|
+
buildClaudeStreamArgs({
|
|
66
|
+
resumeSessionId: 'sid-1',
|
|
67
|
+
appendSystemPrompt: 'You are admin.',
|
|
68
|
+
verbose: false,
|
|
69
|
+
providerArgs: ['--model', 'opus'],
|
|
70
|
+
})
|
|
71
|
+
).toEqual([
|
|
72
|
+
'--output-format',
|
|
73
|
+
'stream-json',
|
|
74
|
+
'--input-format',
|
|
75
|
+
'stream-json',
|
|
76
|
+
'--permission-prompt-tool',
|
|
77
|
+
'stdio',
|
|
78
|
+
// no --verbose because verbose:false
|
|
79
|
+
'--resume',
|
|
80
|
+
'sid-1',
|
|
81
|
+
'--append-system-prompt',
|
|
82
|
+
'You are admin.',
|
|
83
|
+
'--model',
|
|
84
|
+
'opus',
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('omits --resume / --append-system-prompt when their values are blank', () => {
|
|
89
|
+
const args = buildClaudeStreamArgs({ resumeSessionId: ' ', appendSystemPrompt: '' });
|
|
90
|
+
expect(args).not.toContain('--resume');
|
|
91
|
+
expect(args).not.toContain('--append-system-prompt');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('formatClaudeStdinUserTurn', () => {
|
|
96
|
+
it('produces a single NDJSON user line terminated by a newline', () => {
|
|
97
|
+
const out = formatClaudeStdinUserTurn('fix the bug');
|
|
98
|
+
expect(out.endsWith('\n')).toBe(true);
|
|
99
|
+
const parsed = JSON.parse(out.trim());
|
|
100
|
+
expect(parsed).toEqual({
|
|
101
|
+
type: 'user',
|
|
102
|
+
message: { role: 'user', content: [{ type: 'text', text: 'fix the bug' }] },
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('DirectCliSessionManager', () => {
|
|
108
|
+
it('spawns on first ensureSession with cwd=workDir and base args', async () => {
|
|
109
|
+
let spawnArgs: string[] = [];
|
|
110
|
+
let spawnOpts: SpawnOptions = {};
|
|
111
|
+
const child = createFakeChild();
|
|
112
|
+
const manager = new DirectCliSessionManager({
|
|
113
|
+
spawnFn: (_bin, args, opts) => {
|
|
114
|
+
spawnArgs = args;
|
|
115
|
+
spawnOpts = opts;
|
|
116
|
+
return child as unknown as import('child_process').ChildProcess;
|
|
117
|
+
},
|
|
118
|
+
envResolver: async () => ({ env: { X: '1' }, providerArgs: [] }),
|
|
119
|
+
binaryResolver: { resolve: async () => '/fake/claude' } as never,
|
|
120
|
+
store: new Map<string, string>() as never,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await manager.ensureSession({ sessionKey: 't:lead', workDir: '/proj' });
|
|
124
|
+
|
|
125
|
+
expect(manager.has('t:lead')).toBe(true);
|
|
126
|
+
expect(spawnOpts.cwd).toBe('/proj');
|
|
127
|
+
expect(spawnOpts.env).toEqual({ X: '1' });
|
|
128
|
+
expect(spawnArgs).toContain('--output-format');
|
|
129
|
+
expect(spawnArgs).toContain('stream-json');
|
|
130
|
+
expect(spawnArgs).toContain('--verbose');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('does not spawn twice for the same session key (dedupes concurrent ensureSession)', async () => {
|
|
134
|
+
let spawnCount = 0;
|
|
135
|
+
const child = createFakeChild();
|
|
136
|
+
const manager = new DirectCliSessionManager({
|
|
137
|
+
spawnFn: () => {
|
|
138
|
+
spawnCount += 1;
|
|
139
|
+
return child as unknown as import('child_process').ChildProcess;
|
|
140
|
+
},
|
|
141
|
+
envResolver: async () => ({ env: {}, providerArgs: [] }),
|
|
142
|
+
binaryResolver: { resolve: async () => '/fake/claude' } as never,
|
|
143
|
+
store: new Map<string, string>() as never,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await Promise.all([
|
|
147
|
+
manager.ensureSession({ sessionKey: 't:lead', workDir: '/proj' }),
|
|
148
|
+
manager.ensureSession({ sessionKey: 't:lead', workDir: '/proj' }),
|
|
149
|
+
]);
|
|
150
|
+
await manager.ensureSession({ sessionKey: 't:lead', workDir: '/proj' });
|
|
151
|
+
|
|
152
|
+
expect(spawnCount).toBe(1);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('emits init → delta → tool → complete for a real stream and captures session id', async () => {
|
|
156
|
+
const { manager, child } = createManager(['--model', 'sonnet']);
|
|
157
|
+
|
|
158
|
+
const events: string[] = [];
|
|
159
|
+
manager.on('event', (e: { kind: string }) => events.push(e.kind));
|
|
160
|
+
|
|
161
|
+
await manager.send('t:lead', { text: 'fixbug', messageId: 'm1', workDir: '/proj' });
|
|
162
|
+
|
|
163
|
+
// system init carries the claude session id (captured for --resume next time)
|
|
164
|
+
child.stdout.emit(
|
|
165
|
+
'data',
|
|
166
|
+
JSON.stringify({ type: 'system', session_id: 'claude-sid-9', model: 'claude-sonnet-4-6' }) +
|
|
167
|
+
'\n'
|
|
168
|
+
);
|
|
169
|
+
// assistant text delta
|
|
170
|
+
child.stdout.emit(
|
|
171
|
+
'data',
|
|
172
|
+
JSON.stringify({
|
|
173
|
+
type: 'assistant',
|
|
174
|
+
message: { id: 'msg_1', content: [{ type: 'text', text: 'working on it' }] },
|
|
175
|
+
}) + '\n'
|
|
176
|
+
);
|
|
177
|
+
// assistant tool_use
|
|
178
|
+
child.stdout.emit(
|
|
179
|
+
'data',
|
|
180
|
+
JSON.stringify({
|
|
181
|
+
type: 'assistant',
|
|
182
|
+
message: {
|
|
183
|
+
content: [{ type: 'tool_use', id: 'tu', name: 'Bash', input: { command: 'ls' } }],
|
|
184
|
+
},
|
|
185
|
+
}) + '\n'
|
|
186
|
+
);
|
|
187
|
+
// result
|
|
188
|
+
child.stdout.emit(
|
|
189
|
+
'data',
|
|
190
|
+
JSON.stringify({
|
|
191
|
+
type: 'result',
|
|
192
|
+
subtype: 'success',
|
|
193
|
+
result: 'done',
|
|
194
|
+
session_id: 'claude-sid-9',
|
|
195
|
+
}) + '\n'
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
expect(events).toEqual(['init', 'delta', 'tool', 'complete']);
|
|
199
|
+
// getSessionId reads the handle's captured session id
|
|
200
|
+
expect(manager.getSessionId('t:lead')).toBe('claude-sid-9');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('writes the user turn to stdin as NDJSON on send', async () => {
|
|
204
|
+
const child = createFakeChild();
|
|
205
|
+
let written = '';
|
|
206
|
+
child.stdin.write = (data: string) => {
|
|
207
|
+
written = data;
|
|
208
|
+
return true;
|
|
209
|
+
};
|
|
210
|
+
const manager = new DirectCliSessionManager({
|
|
211
|
+
spawnFn: () => child as unknown as import('child_process').ChildProcess,
|
|
212
|
+
envResolver: async () => ({ env: {}, providerArgs: [] }),
|
|
213
|
+
binaryResolver: { resolve: async () => '/fake/claude' } as never,
|
|
214
|
+
store: new Map<string, string>() as never,
|
|
215
|
+
});
|
|
216
|
+
await manager.send('t:lead', { text: 'hello', messageId: 'm1', workDir: '/proj' });
|
|
217
|
+
expect(JSON.parse(written.trim())).toEqual({
|
|
218
|
+
type: 'user',
|
|
219
|
+
message: { role: 'user', content: [{ type: 'text', text: 'hello' }] },
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('falls back to accumulated text when result has no result field', async () => {
|
|
224
|
+
const { manager, child } = createManager();
|
|
225
|
+
await manager.send('t:lead', { text: 'x', messageId: 'm1', workDir: '/proj' });
|
|
226
|
+
child.stdout.emit(
|
|
227
|
+
'data',
|
|
228
|
+
JSON.stringify({
|
|
229
|
+
type: 'assistant',
|
|
230
|
+
message: { content: [{ type: 'text', text: 'partial reply' }] },
|
|
231
|
+
}) + '\n'
|
|
232
|
+
);
|
|
233
|
+
let completeText = '';
|
|
234
|
+
manager.on('event', (e: { kind: string; text?: string }) => {
|
|
235
|
+
if (e.kind === 'complete') completeText = e.text ?? '';
|
|
236
|
+
});
|
|
237
|
+
child.stdout.emit('data', JSON.stringify({ type: 'result', subtype: 'success' }) + '\n');
|
|
238
|
+
expect(completeText).toBe('partial reply');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('emits permission-request when a can_use_tool control_request arrives', async () => {
|
|
242
|
+
const { manager, child } = createManager();
|
|
243
|
+
await manager.send('t:lead', { text: 'x', messageId: 'm1', workDir: '/proj' });
|
|
244
|
+
const events: {
|
|
245
|
+
kind: string;
|
|
246
|
+
requestId?: string;
|
|
247
|
+
subtype?: string;
|
|
248
|
+
toolName?: string;
|
|
249
|
+
runId?: string;
|
|
250
|
+
}[] = [];
|
|
251
|
+
manager.on('event', (e) => {
|
|
252
|
+
if (e.kind === 'permission-request') events.push(e as (typeof events)[number]);
|
|
253
|
+
});
|
|
254
|
+
child.stdout.emit(
|
|
255
|
+
'data',
|
|
256
|
+
JSON.stringify({
|
|
257
|
+
type: 'control_request',
|
|
258
|
+
request_id: 'req_42',
|
|
259
|
+
request: { subtype: 'can_use_tool', tool_name: 'Bash', input: { command: 'ls' } },
|
|
260
|
+
}) + '\n'
|
|
261
|
+
);
|
|
262
|
+
expect(events).toHaveLength(1);
|
|
263
|
+
expect(events[0]).toMatchObject({
|
|
264
|
+
kind: 'permission-request',
|
|
265
|
+
requestId: 'req_42',
|
|
266
|
+
subtype: 'can_use_tool',
|
|
267
|
+
toolName: 'Bash',
|
|
268
|
+
toolInput: { command: 'ls' },
|
|
269
|
+
});
|
|
270
|
+
expect(typeof events[0].runId).toBe('string');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('respondPermission writes a control_response line to stdin (allow/deny)', async () => {
|
|
274
|
+
const { manager, child } = createManager();
|
|
275
|
+
await manager.send('t:lead', { text: 'x', messageId: 'm1', workDir: '/proj' });
|
|
276
|
+
const written: string[] = [];
|
|
277
|
+
child.stdin.write = (data: string) => {
|
|
278
|
+
written.push(data);
|
|
279
|
+
return true;
|
|
280
|
+
};
|
|
281
|
+
manager.respondPermission('t:lead', 'req_42', true);
|
|
282
|
+
manager.respondPermission('t:lead', 'req_43', false, 'User denied');
|
|
283
|
+
expect(written.map((line) => JSON.parse(line.trim()))).toEqual([
|
|
284
|
+
{
|
|
285
|
+
type: 'control_response',
|
|
286
|
+
response: {
|
|
287
|
+
subtype: 'success',
|
|
288
|
+
request_id: 'req_42',
|
|
289
|
+
response: { behavior: 'allow', updatedInput: {} },
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
type: 'control_response',
|
|
294
|
+
response: {
|
|
295
|
+
subtype: 'success',
|
|
296
|
+
request_id: 'req_43',
|
|
297
|
+
response: { behavior: 'deny', message: 'User denied' },
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
]);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('respondPermission passes updatedInput through for AskUserQuestion answers (allow)', async () => {
|
|
304
|
+
const { manager, child } = createManager();
|
|
305
|
+
await manager.send('t:lead', { text: 'x', messageId: 'm1', workDir: '/proj' });
|
|
306
|
+
const written: string[] = [];
|
|
307
|
+
child.stdin.write = (data: string) => {
|
|
308
|
+
written.push(data);
|
|
309
|
+
return true;
|
|
310
|
+
};
|
|
311
|
+
const answers = { 'Pick one': 'A' };
|
|
312
|
+
manager.respondPermission('t:lead', 'req_99', true, undefined, {
|
|
313
|
+
...{ prompt: 'Pick one' },
|
|
314
|
+
answers,
|
|
315
|
+
});
|
|
316
|
+
const parsed = JSON.parse(written[0].trim());
|
|
317
|
+
expect(parsed).toEqual({
|
|
318
|
+
type: 'control_response',
|
|
319
|
+
response: {
|
|
320
|
+
subtype: 'success',
|
|
321
|
+
request_id: 'req_99',
|
|
322
|
+
response: { behavior: 'allow', updatedInput: { prompt: 'Pick one', answers } },
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('respondPermission throws when the session is not running', async () => {
|
|
328
|
+
const { manager } = createManager();
|
|
329
|
+
expect(() => manager.respondPermission('missing:lead', 'req_1', true)).toThrow(
|
|
330
|
+
/is not running/
|
|
331
|
+
);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('ignores parse-error and unknown lines without emitting', async () => {
|
|
335
|
+
const { manager, child } = createManager();
|
|
336
|
+
await manager.send('t:lead', { text: 'x', messageId: 'm1', workDir: '/proj' });
|
|
337
|
+
const events: string[] = [];
|
|
338
|
+
manager.on('event', (e: { kind: string }) => events.push(e.kind));
|
|
339
|
+
child.stdout.emit('data', 'this is not json\n');
|
|
340
|
+
child.stdout.emit('data', JSON.stringify({ type: 'stream_event' }) + '\n');
|
|
341
|
+
expect(events).toEqual([]);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('emits error and drops the handle when the process exits non-zero mid-turn', async () => {
|
|
345
|
+
const { manager, child } = createManager();
|
|
346
|
+
const events: { kind: string; error?: string }[] = [];
|
|
347
|
+
manager.on('event', (e: { kind: string; error?: string }) => events.push(e));
|
|
348
|
+
await manager.send('t:lead', { text: 'x', messageId: 'm1', workDir: '/proj' });
|
|
349
|
+
child.emitExit(1);
|
|
350
|
+
expect(events.some((e) => e.kind === 'error')).toBe(true);
|
|
351
|
+
expect(manager.has('t:lead')).toBe(false);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('synthesizes a complete event on clean exit mid-turn (no stuck bubble)', async () => {
|
|
355
|
+
const { manager, child } = createManager();
|
|
356
|
+
const events: { kind: string; text?: string }[] = [];
|
|
357
|
+
manager.on('event', (e: { kind: string; text?: string }) => events.push(e));
|
|
358
|
+
await manager.send('t:lead', { text: 'x', messageId: 'm1', workDir: '/proj' });
|
|
359
|
+
// No `result` line ever arrives — claude exits cleanly (e.g. bailed after a
|
|
360
|
+
// permission prompt). The turn must still terminate so the optimistic bubble
|
|
361
|
+
// doesn't hang forever.
|
|
362
|
+
child.emitExit(0);
|
|
363
|
+
expect(events.some((e) => e.kind === 'complete')).toBe(true);
|
|
364
|
+
expect(manager.has('t:lead')).toBe(false);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('shutdown kills all live sessions', async () => {
|
|
368
|
+
const killed: string[] = [];
|
|
369
|
+
const child = createFakeChild();
|
|
370
|
+
child.kill = (signal?: string) => {
|
|
371
|
+
killed.push(signal ?? 'none');
|
|
372
|
+
};
|
|
373
|
+
const manager = new DirectCliSessionManager({
|
|
374
|
+
spawnFn: () => child as unknown as import('child_process').ChildProcess,
|
|
375
|
+
envResolver: async () => ({ env: {}, providerArgs: [] }),
|
|
376
|
+
binaryResolver: { resolve: async () => '/fake/claude' } as never,
|
|
377
|
+
store: new Map<string, string>() as never,
|
|
378
|
+
});
|
|
379
|
+
await manager.ensureSession({ sessionKey: 'a:lead', workDir: '/p' });
|
|
380
|
+
await manager.ensureSession({ sessionKey: 'b:lead', workDir: '/p' });
|
|
381
|
+
manager.shutdown();
|
|
382
|
+
expect(killed.length).toBe(2);
|
|
383
|
+
expect(manager.has('a:lead')).toBe(false);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('throws a clear error when workDir is missing', async () => {
|
|
387
|
+
const manager = new DirectCliSessionManager({
|
|
388
|
+
spawnFn: () => createFakeChild() as unknown as import('child_process').ChildProcess,
|
|
389
|
+
envResolver: async () => ({ env: {}, providerArgs: [] }),
|
|
390
|
+
binaryResolver: { resolve: async () => '/fake/claude' } as never,
|
|
391
|
+
store: new Map<string, string>() as never,
|
|
392
|
+
});
|
|
393
|
+
await expect(manager.ensureSession({ sessionKey: 't:lead', workDir: '' })).rejects.toThrow(
|
|
394
|
+
/workDir/
|
|
395
|
+
);
|
|
396
|
+
});
|
|
397
|
+
});
|