@cuongtran001/kanna 1.2.0 → 1.3.1
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/dist/client/assets/{arc-BLk1CZKB.js → arc-DXtN3EWJ.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-B01ZQYte.js → architectureDiagram-3BPJPVTR-CIGqW1z-.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-diL6a3cq.js → blockDiagram-GPEHLZMM-BafNS_H6.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-BY1verL8.js → c4Diagram-AAUBKEIU-YdAgRSis.js} +1 -1
- package/dist/client/assets/channel-BEYTrUAm.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-psdZuajw.js → chunk-2J33WTMH-eiP5y5G-.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-CrwQVBc5.js → chunk-4BX2VUAB-CvLGzPMF.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-9-aj4jKO.js → chunk-55IACEB6-Y-S9Fb8A.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-1FNK9_nB.js → chunk-727SXJPM-6sJH1RI9.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-DFhvd_Ek.js → chunk-AQP2D5EJ-hV1r_IXO.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-B7MLrDa7.js → chunk-FMBD7UC4-CC3L28S5.js} +1 -1
- package/dist/client/assets/{chunk-ND2GUHAM-D4Df05QE.js → chunk-ND2GUHAM-BZ3czhdH.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-Iq17Dznm.js → chunk-QZHKN3VN-Bjyd6Yr-.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-CvnF5Ud6.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CvnF5Ud6.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-BmaZhtXy.js → cose-bilkent-S5V4N54A-CfxdDghE.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG-BCmpfnfP.js → dagre-BM42HDAG-B8CddsbB.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-BIEcKowS.js → diagram-2AECGRRQ-F0yS0fVB.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-CRBABBfv.js → diagram-5GNKFQAL-cW0i0qlh.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-BRxUk16N.js → diagram-KO2AKTUF-BzAKMeJT.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-DPBs4xl1.js → diagram-LMA3HP47-qdC4XqIl.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-BE9rgVv8.js → diagram-OG6HWLK6-CGcpi6Dh.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-D3vgA1xJ.js → erDiagram-TEJ5UH35-CxNTDicM.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-Cgu-8XXC.js → flowDiagram-I6XJVG4X-qkGa5TqY.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-CgjXVyTR.js → ganttDiagram-6RSMTGT7-D9mVPoQi.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-zD5D8B-i.js → gitGraphDiagram-PVQCEYII-DA0grAXe.js} +1 -1
- package/dist/client/assets/{index-DBTdhMbJ.js → index-BqUY1Bm4.js} +1 -1
- package/dist/client/assets/index-CLsOWXff.css +32 -0
- package/dist/client/assets/{index-zQLLeAfl.js → index-TU1k6ov6.js} +253 -243
- package/dist/client/assets/{infoDiagram-5YYISTIA-BaFHqT4u.js → infoDiagram-5YYISTIA-CGljtLs3.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-2yAC1br2.js → ishikawaDiagram-YF4QCWOH-CtC0xrmG.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-DacQ-ND5.js → journeyDiagram-JHISSGLW-URLnVZeN.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-Qlbwm3z4.js → kanban-definition-UN3LZRKU-C2Q6aqQh.js} +1 -1
- package/dist/client/assets/{linear-DeafqpgO.js → linear-ByKHtsIx.js} +1 -1
- package/dist/client/assets/{mermaid.core-KYiCPJDj.js → mermaid.core-Dl_f2KLc.js} +4 -4
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-B1V-T6d3.js → mindmap-definition-RKZ34NQL-CaJ9IDLI.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-DOF9OFRu.js → pieDiagram-4H26LBE5-mmYaf2Oa.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-lH1mB7ko.js → quadrantDiagram-W4KKPZXB-B_PO6OCC.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-BaTsJ_sS.js → requirementDiagram-4Y6WPE33-M4huwJt1.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-Pfxib5iI.js → sankeyDiagram-5OEKKPKP-B0hb1sWh.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-BQMEKkBC.js → sequenceDiagram-3UESZ5HK-BAqSptnA.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-CuldNnzp.js → stateDiagram-AJRCARHV-DTNkvPi5.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CVN3oGTF.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-DP7u0YAg.js → timeline-definition-PNZ67QCA-MgAjHz8Q.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-QlrjpNeQ.js → vennDiagram-CIIHVFJN-DmKoO51W.js} +1 -1
- package/dist/client/assets/{wardley-L42UT6IY-BqdJ_sAM.js → wardley-L42UT6IY-CWcyHnYs.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Wi3kaQ5X.js → wardleyDiagram-YWT4CUSO-BGIGmFP9.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-D-KXMyCG.js → xychartDiagram-2RQKCTM6-D20YIvEl.js} +1 -1
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
- package/src/server/agent-coordinator-types.ts +158 -0
- package/src/server/agent-coordinator.ts +919 -0
- package/src/server/agent-deps-builders.ts +425 -0
- package/src/server/agent.oauth-rotation.test.ts +101 -0
- package/src/server/agent.test.ts +24 -8
- package/src/server/agent.ts +35 -4963
- package/src/server/auto-continue/read-model.test.ts +2 -2
- package/src/server/auto-continue/read-model.ts +4 -1
- package/src/server/claude-autocontinue-commands.test.ts +494 -0
- package/src/server/claude-autocontinue-commands.ts +256 -0
- package/src/server/claude-cancel-handler.test.ts +472 -0
- package/src/server/claude-cancel-handler.ts +241 -0
- package/src/server/claude-chat-management.test.ts +395 -0
- package/src/server/claude-chat-management.ts +251 -0
- package/src/server/claude-harness-stream.test.ts +273 -0
- package/src/server/claude-harness-stream.ts +158 -0
- package/src/server/claude-loop-orch-commands.test.ts +579 -0
- package/src/server/claude-loop-orch-commands.ts +466 -0
- package/src/server/claude-message-normalizer.test.ts +213 -0
- package/src/server/claude-message-normalizer.ts +436 -0
- package/src/server/claude-prompt-helpers.test.ts +248 -0
- package/src/server/claude-prompt-helpers.ts +139 -0
- package/src/server/claude-sdk-queue.test.ts +169 -0
- package/src/server/claude-sdk-queue.ts +82 -0
- package/src/server/claude-send-command.test.ts +414 -0
- package/src/server/claude-send-command.ts +420 -0
- package/src/server/claude-session-config-helpers.test.ts +279 -0
- package/src/server/claude-session-config-helpers.ts +151 -0
- package/src/server/claude-session-config.test.ts +250 -0
- package/src/server/claude-session-config.ts +159 -0
- package/src/server/claude-session-error-handler.test.ts +498 -0
- package/src/server/claude-session-error-handler.ts +361 -0
- package/src/server/claude-session-lifecycle.test.ts +669 -0
- package/src/server/claude-session-lifecycle.ts +283 -0
- package/src/server/claude-session-rebuild.test.ts +224 -0
- package/src/server/claude-session-rebuild.ts +101 -0
- package/src/server/claude-session-runner.test.ts +523 -0
- package/src/server/claude-session-runner.ts +521 -0
- package/src/server/claude-session-spawner.test.ts +401 -0
- package/src/server/claude-session-spawner.ts +401 -0
- package/src/server/claude-session-start.test.ts +205 -0
- package/src/server/claude-session-start.ts +277 -0
- package/src/server/claude-session-state-queries.test.ts +344 -0
- package/src/server/claude-session-state-queries.ts +160 -0
- package/src/server/claude-session-state.ts +104 -0
- package/src/server/claude-slash-commands.test.ts +277 -0
- package/src/server/claude-slash-commands.ts +234 -0
- package/src/server/claude-spawn-helpers.test.ts +221 -0
- package/src/server/claude-spawn-helpers.ts +179 -0
- package/src/server/claude-steer-log.test.ts +243 -0
- package/src/server/claude-steer-log.ts +58 -0
- package/src/server/claude-subagent-tool-response.test.ts +213 -0
- package/src/server/claude-subagent-tool-response.ts +165 -0
- package/src/server/claude-subagent-wiring.test.ts +263 -0
- package/src/server/claude-subagent-wiring.ts +324 -0
- package/src/server/claude-tool-respond.test.ts +321 -0
- package/src/server/claude-tool-respond.ts +142 -0
- package/src/server/claude-turn-runner.test.ts +310 -0
- package/src/server/claude-turn-runner.ts +228 -0
- package/src/server/claude-turn-starter.test.ts +266 -0
- package/src/server/claude-turn-starter.ts +566 -0
- package/src/server/claude-usage-math.test.ts +169 -0
- package/src/server/claude-usage-math.ts +132 -0
- package/src/server/diff-store-branch-ops.adapter.test.ts +332 -0
- package/src/server/diff-store-branch-ops.adapter.ts +602 -0
- package/src/server/diff-store-commit-ops.adapter.test.ts +358 -0
- package/src/server/diff-store-commit-ops.adapter.ts +258 -0
- package/src/server/diff-store-errors.ts +85 -0
- package/src/server/diff-store-file-ops.adapter.ts +212 -0
- package/src/server/diff-store-git-branch.adapter.ts +686 -0
- package/src/server/diff-store-io.adapter.ts +27 -0
- package/src/server/diff-store-parse.test.ts +212 -0
- package/src/server/diff-store-parse.ts +122 -0
- package/src/server/diff-store-state.ts +82 -0
- package/src/server/diff-store.ts +76 -1854
- package/src/server/event-store-apply.test.ts +280 -0
- package/src/server/event-store-apply.ts +132 -0
- package/src/server/event-store-chat-lifecycle.test.ts +408 -0
- package/src/server/event-store-chat-lifecycle.ts +441 -0
- package/src/server/event-store-entity-write.ts +356 -0
- package/src/server/event-store-helpers.test.ts +230 -0
- package/src/server/event-store-helpers.ts +235 -0
- package/src/server/event-store-init.ts +276 -0
- package/src/server/event-store-messages.adapter.test.ts +365 -0
- package/src/server/event-store-messages.adapter.ts +213 -0
- package/src/server/event-store-orch-subscription.ts +124 -0
- package/src/server/event-store-orch.test.ts +462 -0
- package/src/server/event-store-orch.ts +312 -0
- package/src/server/event-store-peripheral-events.adapter.test.ts +254 -0
- package/src/server/event-store-peripheral-events.adapter.ts +105 -0
- package/src/server/event-store-snapshot.test.ts +597 -0
- package/src/server/event-store-snapshot.ts +658 -0
- package/src/server/event-store-subagent.test.ts +408 -0
- package/src/server/event-store-subagent.ts +198 -0
- package/src/server/event-store-tool-requests.test.ts +210 -0
- package/src/server/event-store-tool-requests.ts +131 -0
- package/src/server/event-store-transcript-write.adapter.ts +285 -0
- package/src/server/event-store-write-ops.test.ts +554 -0
- package/src/server/event-store-write-ops.ts +529 -0
- package/src/server/event-store.test.ts +35 -0
- package/src/server/event-store.ts +251 -2284
- package/src/server/events.ts +3 -0
- package/src/server/harness-types.ts +15 -1
- package/src/server/oauth-errors.ts +19 -0
- package/src/server/read-models.ts +26 -1
- package/src/server/subagent-orchestrator.ts +3 -0
- package/src/server/ws-router-agent-ctrl.test.ts +187 -0
- package/src/server/ws-router-agent-ctrl.ts +137 -0
- package/src/server/ws-router-broadcast.ts +488 -0
- package/src/server/ws-router-chat.test.ts +461 -0
- package/src/server/ws-router-chat.ts +279 -0
- package/src/server/ws-router-defaults.test.ts +283 -0
- package/src/server/ws-router-defaults.ts +434 -0
- package/src/server/ws-router-diff.test.ts +167 -0
- package/src/server/ws-router-diff.ts +250 -0
- package/src/server/ws-router-envelope.ts +329 -0
- package/src/server/ws-router-misc.test.ts +422 -0
- package/src/server/ws-router-misc.ts +265 -0
- package/src/server/ws-router-orch.test.ts +190 -0
- package/src/server/ws-router-orch.ts +93 -0
- package/src/server/ws-router-project.test.ts +403 -0
- package/src/server/ws-router-project.ts +256 -0
- package/src/server/ws-router-push.test.ts +193 -0
- package/src/server/ws-router-push.ts +120 -0
- package/src/server/ws-router-settings.test.ts +173 -0
- package/src/server/ws-router-settings.ts +416 -0
- package/src/server/ws-router-skills.test.ts +194 -0
- package/src/server/ws-router-skills.ts +204 -0
- package/src/server/ws-router-utils.ts +224 -0
- package/src/server/ws-router.ts +339 -2254
- package/src/shared/app-settings-types.ts +365 -0
- package/src/shared/core-types.ts +27 -0
- package/src/shared/git-diff-types.ts +165 -0
- package/src/shared/loop-progress.test.ts +106 -0
- package/src/shared/loop-progress.ts +88 -0
- package/src/shared/mcp-types.ts +105 -0
- package/src/shared/provider-model-types.ts +437 -0
- package/src/shared/subagent-types.ts +198 -0
- package/src/shared/tool-call-types.ts +314 -0
- package/src/shared/transcript-types.ts +270 -0
- package/src/shared/types.ts +27 -1668
- package/dist/client/assets/channel-BMkhjiLI.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-C5Kg1XOD.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-C5Kg1XOD.js +0 -1
- package/dist/client/assets/index-H6C-f2Vp.css +0 -32
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Ca2U11af.js +0 -1
package/src/server/ws-router.ts
CHANGED
|
@@ -1,24 +1,15 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto"
|
|
2
|
-
import { readTextFileOrThrow, spawnCommandCapture } from "./ws-router-io.adapter"
|
|
3
1
|
import type { AnyValue } from "../shared/errors"
|
|
4
|
-
import { isRecord } from "../shared/errors"
|
|
5
2
|
import { log } from "../shared/log"
|
|
6
|
-
import os from "node:os"
|
|
7
|
-
import path from "node:path"
|
|
8
3
|
import type { ServerWebSocket } from "bun"
|
|
9
4
|
import { PROTOCOL_VERSION } from "../shared/types"
|
|
10
|
-
import type { ClientEnvelope
|
|
11
|
-
import type { PtyInstanceDelta } from "../shared/pty-instance"
|
|
12
|
-
import type { PtyInstanceRegistry } from "./claude-pty/pty-instance-registry"
|
|
13
|
-
import type { WorkflowRegistry } from "./workflow-registry"
|
|
14
|
-
import type { SubagentTranscriptRegistry } from "./subagent-transcript-registry"
|
|
5
|
+
import type { ClientEnvelope } from "../shared/protocol"
|
|
15
6
|
import { isClientEnvelope } from "../shared/protocol"
|
|
16
7
|
import type { AgentCoordinator } from "./agent"
|
|
17
8
|
import type { AnalyticsReporter } from "./analytics"
|
|
18
9
|
import { NoopAnalyticsReporter } from "./analytics"
|
|
19
10
|
import type { AppSettingsManager } from "./app-settings"
|
|
20
11
|
import type { DiscoveredProject } from "./discovery.adapter"
|
|
21
|
-
import { DiffStore
|
|
12
|
+
import { DiffStore } from "./diff-store"
|
|
22
13
|
import { EventStore } from "./event-store"
|
|
23
14
|
import { openExternal } from "./external-open"
|
|
24
15
|
import { KeybindingsManager } from "./keybindings"
|
|
@@ -26,116 +17,58 @@ import { resolveLocalPath } from "./paths"
|
|
|
26
17
|
import { ensureProjectDirectory } from "./project-directory.adapter"
|
|
27
18
|
import { TerminalManager } from "./terminal-manager"
|
|
28
19
|
import type { UpdateManager } from "./update-manager"
|
|
29
|
-
import { deriveChatSnapshot, deriveLocalProjectsSnapshot, deriveSidebarData } from "./read-models"
|
|
30
|
-
import { toOrchRunSummary } from "./orchestration-input"
|
|
31
|
-
import { AUTH_DEFAULTS, CLAUDE_AUTH_DEFAULTS, CLAUDE_DRIVER_DEFAULTS, CLAUDE_PTY_LIFECYCLE_DEFAULTS, CLOUDFLARE_TUNNEL_DEFAULTS, DEFAULT_OPENROUTER_SDK_MODEL, UPLOAD_DEFAULTS } from "../shared/types"
|
|
32
20
|
import type {
|
|
33
|
-
AppSettingsPatch,
|
|
34
|
-
AppSettingsSnapshot,
|
|
35
|
-
InstalledSkillsSnapshot,
|
|
36
|
-
McpServerConfig,
|
|
37
21
|
LlmProviderSnapshot,
|
|
38
22
|
LlmProviderValidationResult,
|
|
39
23
|
OpenRouterModel,
|
|
40
|
-
SkillInstallResult,
|
|
41
|
-
SkillSearchSnapshot,
|
|
42
|
-
SkillUninstallResult,
|
|
43
|
-
Subagent,
|
|
44
|
-
SubagentValidationError,
|
|
45
24
|
} from "../shared/types"
|
|
46
25
|
import { importClaudeSessions } from "./claude-session-importer.adapter"
|
|
47
26
|
import { listWorktrees } from "./worktree-store.adapter"
|
|
48
27
|
import type { TunnelGateway } from "./cloudflare-tunnel/gateway"
|
|
49
28
|
import type { PushManager } from "./push/push-manager"
|
|
50
|
-
import { validateMcpServer } from "./mcp-validator"
|
|
51
|
-
import { startMcpOAuth, completeMcpOAuth, ensureFreshMcpToken } from "./mcp-oauth.adapter"
|
|
52
29
|
import type { SessionShareService } from "./session-share"
|
|
53
|
-
import type {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
case "chat":
|
|
96
|
-
chat += 1
|
|
97
|
-
break
|
|
98
|
-
case "project-git":
|
|
99
|
-
projectGit += 1
|
|
100
|
-
break
|
|
101
|
-
case "local-projects":
|
|
102
|
-
localProjects += 1
|
|
103
|
-
break
|
|
104
|
-
case "update":
|
|
105
|
-
update += 1
|
|
106
|
-
break
|
|
107
|
-
case "keybindings":
|
|
108
|
-
keybindings += 1
|
|
109
|
-
break
|
|
110
|
-
case "app-settings":
|
|
111
|
-
appSettings += 1
|
|
112
|
-
break
|
|
113
|
-
case "terminal":
|
|
114
|
-
terminal += 1
|
|
115
|
-
break
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
total: ws.data.subscriptions.size,
|
|
121
|
-
sidebar,
|
|
122
|
-
chat,
|
|
123
|
-
projectGit,
|
|
124
|
-
localProjects,
|
|
125
|
-
update,
|
|
126
|
-
keybindings,
|
|
127
|
-
appSettings,
|
|
128
|
-
terminal,
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export interface ClientState {
|
|
133
|
-
subscriptions: Map<string, SubscriptionTopic>
|
|
134
|
-
snapshotSignatures: Map<string, string>
|
|
135
|
-
protectedDraftChatIds?: Set<string>
|
|
136
|
-
pushDeviceId?: string | null
|
|
137
|
-
originHost?: string
|
|
138
|
-
}
|
|
30
|
+
import type { PtyInstanceRegistry } from "./claude-pty/pty-instance-registry"
|
|
31
|
+
import type { WorkflowRegistry } from "./workflow-registry"
|
|
32
|
+
import type { SubagentTranscriptRegistry } from "./subagent-transcript-registry"
|
|
33
|
+
import { buildFallbackDiffStore, buildFallbackLlmProvider, buildResolvedAppSettings } from "./ws-router-defaults"
|
|
34
|
+
import { handleSettingsCommand } from "./ws-router-settings"
|
|
35
|
+
import { handleDiffCommand } from "./ws-router-diff"
|
|
36
|
+
import { handleOrchCommand } from "./ws-router-orch"
|
|
37
|
+
import { handleAgentCtrlCommand } from "./ws-router-agent-ctrl"
|
|
38
|
+
import { handlePushCommand } from "./ws-router-push"
|
|
39
|
+
import { handleMiscCommand } from "./ws-router-misc"
|
|
40
|
+
import { handleProjectCommand } from "./ws-router-project"
|
|
41
|
+
import { handleChatCommand } from "./ws-router-chat"
|
|
42
|
+
import {
|
|
43
|
+
ensureSnapshotSignatures,
|
|
44
|
+
isBenignStaleStateMessage,
|
|
45
|
+
logSendToStartingProfile,
|
|
46
|
+
send,
|
|
47
|
+
} from "./ws-router-utils"
|
|
48
|
+
import type { ClientState } from "./ws-router-utils"
|
|
49
|
+
import { createEnvelopeBuilder } from "./ws-router-envelope"
|
|
50
|
+
import { BroadcastManager } from "./ws-router-broadcast"
|
|
51
|
+
|
|
52
|
+
// Re-export skill utilities so existing callers (tests, server.ts, etc.) keep working.
|
|
53
|
+
export {
|
|
54
|
+
assertSafeSkillId,
|
|
55
|
+
assertSafeSkillSource,
|
|
56
|
+
buildInstallSkillCommand,
|
|
57
|
+
buildUninstallSkillCommand,
|
|
58
|
+
getGlobalSkillLockPath,
|
|
59
|
+
installSkill,
|
|
60
|
+
listInstalledSkills,
|
|
61
|
+
parseInstalledSkillsLock,
|
|
62
|
+
searchSkills,
|
|
63
|
+
uninstallSkill,
|
|
64
|
+
} from "./ws-router-skills"
|
|
65
|
+
|
|
66
|
+
// Re-export settings helpers that tests import from this module.
|
|
67
|
+
export { resolveMcpTestBearer } from "./ws-router-settings"
|
|
68
|
+
|
|
69
|
+
// Re-export for backwards compatibility — callers import these from ws-router.
|
|
70
|
+
export type { ClientState } from "./ws-router-utils"
|
|
71
|
+
export { isBenignStaleStateMessage } from "./ws-router-utils"
|
|
139
72
|
|
|
140
73
|
interface CreateWsRouterArgs {
|
|
141
74
|
store: EventStore
|
|
@@ -165,249 +98,6 @@ interface CreateWsRouterArgs {
|
|
|
165
98
|
sessionShare?: SessionShareService
|
|
166
99
|
}
|
|
167
100
|
|
|
168
|
-
interface SnapshotBroadcastFilter {
|
|
169
|
-
includeSidebar?: boolean
|
|
170
|
-
includeLocalProjects?: boolean
|
|
171
|
-
includeUpdate?: boolean
|
|
172
|
-
includeKeybindings?: boolean
|
|
173
|
-
includeAppSettings?: boolean
|
|
174
|
-
includePushConfig?: boolean
|
|
175
|
-
chatIds?: Set<string>
|
|
176
|
-
projectIds?: Set<string>
|
|
177
|
-
terminalIds?: Set<string>
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
interface SnapshotComputationCache {
|
|
181
|
-
sidebar?: {
|
|
182
|
-
data: ReturnType<typeof deriveSidebarData>
|
|
183
|
-
signature: string
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function getSidebarProjectOrder(store: EventStore) {
|
|
188
|
-
return typeof store.getSidebarProjectOrder === "function"
|
|
189
|
-
? store.getSidebarProjectOrder()
|
|
190
|
-
: []
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Stale-state command failures happen during normal client/server races
|
|
194
|
-
// (e.g. the user steers a queued message that drained between snapshots).
|
|
195
|
-
// They flood pm2 logs at console.error level; downgrade to console.log.
|
|
196
|
-
const BENIGN_STALE_STATE_MESSAGES = [
|
|
197
|
-
/^Chat not found$/,
|
|
198
|
-
/^Queued message not found$/,
|
|
199
|
-
/^File is no longer changed: /,
|
|
200
|
-
/^Project not found$/,
|
|
201
|
-
] as const
|
|
202
|
-
|
|
203
|
-
export function isBenignStaleStateMessage(message: string): boolean {
|
|
204
|
-
return BENIGN_STALE_STATE_MESSAGES.some((pattern) => pattern.test(message))
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function send(ws: ServerWebSocket<ClientState>, message: ServerEnvelope) {
|
|
208
|
-
const payload = JSON.stringify(message)
|
|
209
|
-
ws.send(payload)
|
|
210
|
-
return payload.length
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function isSubagentValidationError(value: Subagent | SubagentValidationError): value is SubagentValidationError {
|
|
214
|
-
return "code" in value && "message" in value
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export function assertSafeSkillSource(source: string) {
|
|
218
|
-
const normalized = source.trim()
|
|
219
|
-
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(normalized)) {
|
|
220
|
-
throw new Error("Skill source must be an owner/repo pair.")
|
|
221
|
-
}
|
|
222
|
-
return normalized
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
export function assertSafeSkillId(skillId: string) {
|
|
226
|
-
const normalized = skillId.trim()
|
|
227
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(normalized)) {
|
|
228
|
-
throw new Error("Skill id is invalid.")
|
|
229
|
-
}
|
|
230
|
-
return normalized
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
export function getGlobalSkillLockPath() {
|
|
234
|
-
const xdgStateHome = process.env.XDG_STATE_HOME?.trim()
|
|
235
|
-
if (xdgStateHome) {
|
|
236
|
-
return path.join(xdgStateHome, "skills", ".skill-lock.json")
|
|
237
|
-
}
|
|
238
|
-
return path.join(os.homedir(), ".agents", ".skill-lock.json")
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function asString(value: AnyValue) {
|
|
242
|
-
return typeof value === "string" ? value : ""
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
export function parseInstalledSkillsLock(parsed: AnyValue, lockFilePath: string): InstalledSkillsSnapshot {
|
|
246
|
-
const skillsRaw = isRecord(parsed) && isRecord(parsed.skills) && !Array.isArray(parsed.skills)
|
|
247
|
-
? parsed.skills
|
|
248
|
-
: null
|
|
249
|
-
const skillsRecord: Record<string, AnyValue> = skillsRaw ?? {}
|
|
250
|
-
|
|
251
|
-
const skills = Object.entries(skillsRecord)
|
|
252
|
-
.filter(([, entry]) => entry && typeof entry === "object" && !Array.isArray(entry))
|
|
253
|
-
.map(([name, entry]) => {
|
|
254
|
-
const record: Record<string, AnyValue> = isRecord(entry) ? entry : {}
|
|
255
|
-
return {
|
|
256
|
-
name,
|
|
257
|
-
source: asString(record.source),
|
|
258
|
-
sourceType: asString(record.sourceType),
|
|
259
|
-
sourceUrl: asString(record.sourceUrl),
|
|
260
|
-
skillPath: asString(record.skillPath) || undefined,
|
|
261
|
-
installedAt: asString(record.installedAt),
|
|
262
|
-
updatedAt: asString(record.updatedAt),
|
|
263
|
-
pluginName: asString(record.pluginName) || undefined,
|
|
264
|
-
}
|
|
265
|
-
})
|
|
266
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
267
|
-
|
|
268
|
-
return {
|
|
269
|
-
lockFilePath,
|
|
270
|
-
skills,
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
export async function listInstalledSkills(lockFilePath = getGlobalSkillLockPath()): Promise<InstalledSkillsSnapshot> {
|
|
275
|
-
try {
|
|
276
|
-
return parseInstalledSkillsLock(JSON.parse(await readTextFileOrThrow(lockFilePath)), lockFilePath)
|
|
277
|
-
} catch {
|
|
278
|
-
return {
|
|
279
|
-
lockFilePath,
|
|
280
|
-
skills: [],
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
export async function searchSkills(query: string, limit = 100): Promise<SkillSearchSnapshot> {
|
|
286
|
-
const normalizedQuery = query.trim()
|
|
287
|
-
if (normalizedQuery.length < 2) {
|
|
288
|
-
return {
|
|
289
|
-
query: normalizedQuery,
|
|
290
|
-
searchType: "fuzzy",
|
|
291
|
-
skills: [],
|
|
292
|
-
count: 0,
|
|
293
|
-
duration_ms: 0,
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const normalizedLimit = Math.max(1, Math.min(100, Math.trunc(limit)))
|
|
298
|
-
const url = new URL("https://skills.sh/api/search")
|
|
299
|
-
url.searchParams.set("q", normalizedQuery)
|
|
300
|
-
url.searchParams.set("limit", String(normalizedLimit))
|
|
301
|
-
|
|
302
|
-
const response = await fetch(url, {
|
|
303
|
-
signal: AbortSignal.timeout(10_000),
|
|
304
|
-
})
|
|
305
|
-
if (!response.ok) {
|
|
306
|
-
throw new Error(`Skills search failed with status ${response.status}.`)
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const payload: Partial<SkillSearchSnapshot> = await response.json()
|
|
310
|
-
return {
|
|
311
|
-
query: typeof payload.query === "string" ? payload.query : normalizedQuery,
|
|
312
|
-
searchType: typeof payload.searchType === "string" ? payload.searchType : "fuzzy",
|
|
313
|
-
skills: Array.isArray(payload.skills)
|
|
314
|
-
? payload.skills
|
|
315
|
-
.filter((skill) => (
|
|
316
|
-
skill
|
|
317
|
-
&& typeof skill === "object"
|
|
318
|
-
&& typeof skill.id === "string"
|
|
319
|
-
&& typeof skill.skillId === "string"
|
|
320
|
-
&& typeof skill.name === "string"
|
|
321
|
-
&& typeof skill.source === "string"
|
|
322
|
-
))
|
|
323
|
-
.map((skill) => ({
|
|
324
|
-
id: skill.id,
|
|
325
|
-
skillId: skill.skillId,
|
|
326
|
-
name: skill.name,
|
|
327
|
-
installs: typeof skill.installs === "number" ? skill.installs : 0,
|
|
328
|
-
source: skill.source,
|
|
329
|
-
}))
|
|
330
|
-
: [],
|
|
331
|
-
count: typeof payload.count === "number" ? payload.count : 0,
|
|
332
|
-
duration_ms: typeof payload.duration_ms === "number" ? payload.duration_ms : 0,
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
export function buildInstallSkillCommand(source: string, skillId: string) {
|
|
337
|
-
return [
|
|
338
|
-
process.platform === "win32" ? "npx.cmd" : "npx",
|
|
339
|
-
"skills",
|
|
340
|
-
"add",
|
|
341
|
-
assertSafeSkillSource(source),
|
|
342
|
-
"--skill",
|
|
343
|
-
assertSafeSkillId(skillId),
|
|
344
|
-
"--global",
|
|
345
|
-
"--agent",
|
|
346
|
-
...SKILL_AGENT_ALIASES,
|
|
347
|
-
"--yes",
|
|
348
|
-
]
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
export function buildUninstallSkillCommand(skillId: string) {
|
|
352
|
-
return [
|
|
353
|
-
process.platform === "win32" ? "npx.cmd" : "npx",
|
|
354
|
-
"skills",
|
|
355
|
-
"remove",
|
|
356
|
-
assertSafeSkillId(skillId),
|
|
357
|
-
"--global",
|
|
358
|
-
"--agent",
|
|
359
|
-
...SKILL_AGENT_ALIASES,
|
|
360
|
-
"--yes",
|
|
361
|
-
]
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
async function runSkillCommand(command: string[]) {
|
|
365
|
-
const cwd = os.homedir()
|
|
366
|
-
const { stdout, stderr, exitCode } = await spawnCommandCapture(command, cwd, {
|
|
367
|
-
...process.env,
|
|
368
|
-
DISABLE_TELEMETRY: process.env.DISABLE_TELEMETRY ?? "1",
|
|
369
|
-
})
|
|
370
|
-
|
|
371
|
-
if (exitCode !== 0) {
|
|
372
|
-
throw new Error(stderr.trim() || stdout.trim() || `skills CLI exited with code ${exitCode}.`)
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
return { cwd, stdout, stderr }
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
export async function installSkill(source: string, skillId: string): Promise<SkillInstallResult> {
|
|
379
|
-
const command = buildInstallSkillCommand(source, skillId)
|
|
380
|
-
const { cwd, stdout, stderr } = await runSkillCommand(command)
|
|
381
|
-
return {
|
|
382
|
-
source: command[3],
|
|
383
|
-
skillId: command[5],
|
|
384
|
-
command,
|
|
385
|
-
cwd,
|
|
386
|
-
stdout,
|
|
387
|
-
stderr,
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
export async function uninstallSkill(skillId: string): Promise<SkillUninstallResult> {
|
|
392
|
-
const command = buildUninstallSkillCommand(skillId)
|
|
393
|
-
const { cwd, stdout, stderr } = await runSkillCommand(command)
|
|
394
|
-
return {
|
|
395
|
-
skillId: command[3],
|
|
396
|
-
command,
|
|
397
|
-
cwd,
|
|
398
|
-
stdout,
|
|
399
|
-
stderr,
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function ensureSnapshotSignatures(ws: ServerWebSocket<ClientState>) {
|
|
404
|
-
if (!ws.data.snapshotSignatures) {
|
|
405
|
-
ws.data.snapshotSignatures = new Map()
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
return ws.data.snapshotSignatures
|
|
409
|
-
}
|
|
410
|
-
|
|
411
101
|
export function createWsRouter({
|
|
412
102
|
store,
|
|
413
103
|
diffStore,
|
|
@@ -430,908 +120,37 @@ export function createWsRouter({
|
|
|
430
120
|
subagentTranscriptRegistry,
|
|
431
121
|
sessionShare,
|
|
432
122
|
}: CreateWsRouterArgs) {
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const pendingBroadcastChatIds = new Set<string>()
|
|
437
|
-
const resolvedDiffStore = diffStore ?? {
|
|
438
|
-
getProjectSnapshot: () => ({ status: "unknown", branchName: undefined, defaultBranchName: undefined, hasOriginRemote: undefined, originRepoSlug: undefined, hasUpstream: undefined, aheadCount: undefined, behindCount: undefined, lastFetchedAt: undefined, files: [] as const, branchHistory: { entries: [] as const } }),
|
|
439
|
-
refreshSnapshot: async () => false,
|
|
440
|
-
initializeGit: async () => ({ ok: true, branchName: undefined, snapshotChanged: false }),
|
|
441
|
-
getGitHubPublishInfo: async () => ({ ghInstalled: false, authenticated: false, activeAccountLogin: undefined, owners: [], suggestedRepoName: "my-repo" }),
|
|
442
|
-
checkGitHubRepoAvailability: async () => ({ available: false, message: "Unavailable" }),
|
|
443
|
-
publishToGitHub: async () => ({ ok: false, title: "Publish failed", message: "Unavailable", snapshotChanged: false }),
|
|
444
|
-
listBranches: async () => ({ recent: [], local: [], remote: [], pullRequests: [], pullRequestsStatus: "unavailable" as const }),
|
|
445
|
-
previewMergeBranch: async () => ({ currentBranchName: undefined, targetBranchName: "", targetDisplayName: "", status: "error" as const, commitCount: 0, hasConflicts: false, message: "Merge preview unavailable." }),
|
|
446
|
-
mergeBranch: async () => ({ ok: false as const, title: "Merge failed", message: "Merge unavailable.", snapshotChanged: false }),
|
|
447
|
-
syncBranch: async () => ({ ok: true, action: "fetch" as const, branchName: undefined, snapshotChanged: false }),
|
|
448
|
-
checkoutBranch: async () => ({ ok: true, branchName: undefined, snapshotChanged: false }),
|
|
449
|
-
createBranch: async () => ({ ok: true, branchName: "main", snapshotChanged: false }),
|
|
450
|
-
generateCommitMessage: async () => ({ subject: "Update selected files", body: "", usedFallback: true, failureMessage: null }),
|
|
451
|
-
commitFiles: async () => ({ ok: true, mode: "commit_only" as const, branchName: undefined, pushed: false, snapshotChanged: false }),
|
|
452
|
-
discardFile: async () => ({ snapshotChanged: false }),
|
|
453
|
-
ignoreFile: async () => ({ snapshotChanged: false }),
|
|
454
|
-
readPatch: async () => ({ patch: "" }),
|
|
455
|
-
}
|
|
456
|
-
const resolvedLlmProvider = llmProvider ?? {
|
|
457
|
-
read: async () => ({
|
|
458
|
-
provider: "openai" as const,
|
|
459
|
-
apiKey: "",
|
|
460
|
-
model: "gpt-5.4-mini",
|
|
461
|
-
baseUrl: "",
|
|
462
|
-
resolvedBaseUrl: "https://api.openai.com/v1",
|
|
463
|
-
enabled: false,
|
|
464
|
-
warning: null,
|
|
465
|
-
filePathDisplay: "~/.kanna/llm-provider.json",
|
|
466
|
-
}),
|
|
467
|
-
write: async ({ provider, apiKey, model, baseUrl }: {
|
|
468
|
-
provider: "openai" | "openrouter" | "custom"
|
|
469
|
-
apiKey: string
|
|
470
|
-
model: string
|
|
471
|
-
baseUrl: string
|
|
472
|
-
}) => {
|
|
473
|
-
let resolvedBaseUrl: string
|
|
474
|
-
if (provider === "openrouter") {
|
|
475
|
-
resolvedBaseUrl = "https://openrouter.ai/api/v1"
|
|
476
|
-
} else if (provider === "custom") {
|
|
477
|
-
resolvedBaseUrl = baseUrl
|
|
478
|
-
} else {
|
|
479
|
-
resolvedBaseUrl = "https://api.openai.com/v1"
|
|
480
|
-
}
|
|
481
|
-
return {
|
|
482
|
-
provider,
|
|
483
|
-
apiKey,
|
|
484
|
-
model,
|
|
485
|
-
baseUrl,
|
|
486
|
-
resolvedBaseUrl,
|
|
487
|
-
enabled: false,
|
|
488
|
-
warning: null,
|
|
489
|
-
filePathDisplay: "~/.kanna/llm-provider.json",
|
|
490
|
-
}
|
|
491
|
-
},
|
|
492
|
-
validate: async () => ({
|
|
493
|
-
ok: false,
|
|
494
|
-
error: {
|
|
495
|
-
type: "config_error",
|
|
496
|
-
message: "LLM provider validation unavailable.",
|
|
497
|
-
},
|
|
498
|
-
}),
|
|
499
|
-
}
|
|
500
|
-
let fallbackAppSettingsSnapshot: AppSettingsSnapshot = {
|
|
501
|
-
analyticsEnabled: true,
|
|
502
|
-
browserSettingsMigrated: false,
|
|
503
|
-
theme: "system",
|
|
504
|
-
chatSoundPreference: "always",
|
|
505
|
-
chatSoundId: "funk",
|
|
506
|
-
terminal: {
|
|
507
|
-
scrollbackLines: 1_000,
|
|
508
|
-
minColumnWidth: 450,
|
|
509
|
-
},
|
|
510
|
-
editor: {
|
|
511
|
-
preset: "cursor",
|
|
512
|
-
commandTemplate: "cursor {path}",
|
|
513
|
-
},
|
|
514
|
-
defaultProvider: "last_used",
|
|
515
|
-
providerDefaults: {
|
|
516
|
-
claude: {
|
|
517
|
-
model: "claude-opus-4-7",
|
|
518
|
-
modelOptions: {
|
|
519
|
-
reasoningEffort: "high",
|
|
520
|
-
contextWindow: "200k",
|
|
521
|
-
},
|
|
522
|
-
planMode: false,
|
|
523
|
-
},
|
|
524
|
-
codex: {
|
|
525
|
-
model: "gpt-5.5",
|
|
526
|
-
modelOptions: {
|
|
527
|
-
reasoningEffort: "high",
|
|
528
|
-
fastMode: false,
|
|
529
|
-
},
|
|
530
|
-
planMode: false,
|
|
531
|
-
},
|
|
532
|
-
openrouter: {
|
|
533
|
-
model: DEFAULT_OPENROUTER_SDK_MODEL,
|
|
534
|
-
modelOptions: {},
|
|
535
|
-
planMode: false,
|
|
536
|
-
},
|
|
537
|
-
},
|
|
538
|
-
warning: null,
|
|
539
|
-
filePathDisplay: "~/.kanna/data/settings.json",
|
|
540
|
-
cloudflareTunnel: CLOUDFLARE_TUNNEL_DEFAULTS,
|
|
541
|
-
auth: AUTH_DEFAULTS,
|
|
542
|
-
claudeAuth: CLAUDE_AUTH_DEFAULTS,
|
|
543
|
-
uploads: UPLOAD_DEFAULTS,
|
|
544
|
-
subagents: [],
|
|
545
|
-
customMcpServers: [],
|
|
546
|
-
customModels: [],
|
|
547
|
-
textSnippets: [],
|
|
548
|
-
claudeDriver: { ...CLAUDE_DRIVER_DEFAULTS, lifecycle: { ...CLAUDE_PTY_LIFECYCLE_DEFAULTS } },
|
|
549
|
-
globalPromptAppend: "",
|
|
550
|
-
shareDefaultTtlHours: 24,
|
|
551
|
-
subagentRuntime: { runTimeoutMs: 600_000, defaultLoopSubagentId: null },
|
|
552
|
-
}
|
|
553
|
-
const mergeAppSettingsPatch = (snapshot: AppSettingsSnapshot, patch: AppSettingsPatch): AppSettingsSnapshot => {
|
|
554
|
-
let subagents = snapshot.subagents
|
|
555
|
-
if (patch.subagents?.create) {
|
|
556
|
-
const now = Date.now()
|
|
557
|
-
subagents = [...subagents, {
|
|
558
|
-
id: randomUUID(),
|
|
559
|
-
...patch.subagents.create,
|
|
560
|
-
name: patch.subagents.create.name.trim(),
|
|
561
|
-
triggerMode: patch.subagents.create.triggerMode ?? "auto",
|
|
562
|
-
createdAt: now,
|
|
563
|
-
updatedAt: now,
|
|
564
|
-
}]
|
|
565
|
-
} else if (patch.subagents?.update) {
|
|
566
|
-
subagents = subagents.map((subagent): Subagent => subagent.id === patch.subagents?.update?.id
|
|
567
|
-
? {
|
|
568
|
-
...subagent,
|
|
569
|
-
...patch.subagents.update.patch,
|
|
570
|
-
name: patch.subagents.update.patch.name?.trim() ?? subagent.name,
|
|
571
|
-
description: patch.subagents.update.patch.description === null
|
|
572
|
-
? undefined
|
|
573
|
-
: patch.subagents.update.patch.description ?? subagent.description,
|
|
574
|
-
modelOptions: <Subagent["modelOptions"]>{ ...subagent.modelOptions, ...(patch.subagents.update.patch.modelOptions ?? {}) },
|
|
575
|
-
workingDir: patch.subagents.update.patch.workingDir === null
|
|
576
|
-
? undefined
|
|
577
|
-
: patch.subagents.update.patch.workingDir ?? subagent.workingDir,
|
|
578
|
-
allowedPaths: patch.subagents.update.patch.allowedPaths === null
|
|
579
|
-
? undefined
|
|
580
|
-
: patch.subagents.update.patch.allowedPaths ?? subagent.allowedPaths,
|
|
581
|
-
maxTurns: patch.subagents.update.patch.maxTurns === null
|
|
582
|
-
? undefined
|
|
583
|
-
: patch.subagents.update.patch.maxTurns ?? subagent.maxTurns,
|
|
584
|
-
updatedAt: Date.now(),
|
|
585
|
-
}
|
|
586
|
-
: subagent)
|
|
587
|
-
} else if (patch.subagents?.delete) {
|
|
588
|
-
subagents = subagents.filter((subagent) => subagent.id !== patch.subagents?.delete?.id)
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
return {
|
|
592
|
-
...snapshot,
|
|
593
|
-
...patch,
|
|
594
|
-
terminal: {
|
|
595
|
-
...snapshot.terminal,
|
|
596
|
-
...patch.terminal,
|
|
597
|
-
},
|
|
598
|
-
editor: {
|
|
599
|
-
...snapshot.editor,
|
|
600
|
-
...patch.editor,
|
|
601
|
-
},
|
|
602
|
-
providerDefaults: {
|
|
603
|
-
claude: {
|
|
604
|
-
...snapshot.providerDefaults.claude,
|
|
605
|
-
...patch.providerDefaults?.claude,
|
|
606
|
-
modelOptions: {
|
|
607
|
-
...snapshot.providerDefaults.claude.modelOptions,
|
|
608
|
-
...patch.providerDefaults?.claude?.modelOptions,
|
|
609
|
-
},
|
|
610
|
-
},
|
|
611
|
-
codex: {
|
|
612
|
-
...snapshot.providerDefaults.codex,
|
|
613
|
-
...patch.providerDefaults?.codex,
|
|
614
|
-
modelOptions: {
|
|
615
|
-
...snapshot.providerDefaults.codex.modelOptions,
|
|
616
|
-
...patch.providerDefaults?.codex?.modelOptions,
|
|
617
|
-
},
|
|
618
|
-
},
|
|
619
|
-
openrouter: {
|
|
620
|
-
...snapshot.providerDefaults.openrouter,
|
|
621
|
-
...patch.providerDefaults?.openrouter,
|
|
622
|
-
modelOptions: {},
|
|
623
|
-
},
|
|
624
|
-
},
|
|
625
|
-
cloudflareTunnel: {
|
|
626
|
-
...snapshot.cloudflareTunnel,
|
|
627
|
-
...patch.cloudflareTunnel,
|
|
628
|
-
},
|
|
629
|
-
auth: {
|
|
630
|
-
...snapshot.auth,
|
|
631
|
-
...patch.auth,
|
|
632
|
-
},
|
|
633
|
-
claudeAuth: {
|
|
634
|
-
tokens: patch.claudeAuth?.tokens ?? snapshot.claudeAuth.tokens,
|
|
635
|
-
concurrencyDefault: patch.claudeAuth?.concurrencyDefault ?? snapshot.claudeAuth.concurrencyDefault,
|
|
636
|
-
},
|
|
637
|
-
uploads: {
|
|
638
|
-
...snapshot.uploads,
|
|
639
|
-
...patch.uploads,
|
|
640
|
-
},
|
|
641
|
-
subagents,
|
|
642
|
-
customMcpServers: snapshot.customMcpServers,
|
|
643
|
-
customModels: snapshot.customModels,
|
|
644
|
-
textSnippets: snapshot.textSnippets,
|
|
645
|
-
claudeDriver: {
|
|
646
|
-
preference: patch.claudeDriver?.preference ?? snapshot.claudeDriver.preference,
|
|
647
|
-
lifecycle: {
|
|
648
|
-
...snapshot.claudeDriver.lifecycle,
|
|
649
|
-
...patch.claudeDriver?.lifecycle,
|
|
650
|
-
},
|
|
651
|
-
},
|
|
652
|
-
subagentRuntime: {
|
|
653
|
-
runTimeoutMs: patch.subagentRuntime?.runTimeoutMs ?? snapshot.subagentRuntime.runTimeoutMs,
|
|
654
|
-
defaultLoopSubagentId: patch.subagentRuntime?.defaultLoopSubagentId !== undefined
|
|
655
|
-
? patch.subagentRuntime.defaultLoopSubagentId
|
|
656
|
-
: snapshot.subagentRuntime.defaultLoopSubagentId,
|
|
657
|
-
},
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
const resolvedAppSettings = {
|
|
661
|
-
getSnapshot: () => appSettings?.getSnapshot() ?? fallbackAppSettingsSnapshot,
|
|
662
|
-
write: async (value: { analyticsEnabled: boolean }) => {
|
|
663
|
-
if (appSettings) return await appSettings.write(value)
|
|
664
|
-
fallbackAppSettingsSnapshot = { ...fallbackAppSettingsSnapshot, analyticsEnabled: value.analyticsEnabled }
|
|
665
|
-
return fallbackAppSettingsSnapshot
|
|
666
|
-
},
|
|
667
|
-
writePatch: async (patch: AppSettingsPatch) => {
|
|
668
|
-
if (appSettings?.writePatch) return await appSettings.writePatch(patch)
|
|
669
|
-
if (appSettings && patch.analyticsEnabled !== undefined && Object.keys(patch).length === 1) {
|
|
670
|
-
return await appSettings.write({ analyticsEnabled: patch.analyticsEnabled })
|
|
671
|
-
}
|
|
672
|
-
fallbackAppSettingsSnapshot = mergeAppSettingsPatch(appSettings?.getSnapshot() ?? fallbackAppSettingsSnapshot, patch)
|
|
673
|
-
return fallbackAppSettingsSnapshot
|
|
674
|
-
},
|
|
675
|
-
setCloudflareTunnel: async (patch: Partial<AppSettingsSnapshot["cloudflareTunnel"]>) => {
|
|
676
|
-
if (appSettings?.setCloudflareTunnel) return await appSettings.setCloudflareTunnel(patch)
|
|
677
|
-
fallbackAppSettingsSnapshot = mergeAppSettingsPatch(appSettings?.getSnapshot() ?? fallbackAppSettingsSnapshot, { cloudflareTunnel: patch })
|
|
678
|
-
return fallbackAppSettingsSnapshot
|
|
679
|
-
},
|
|
680
|
-
setClaudeAuth: async (patch: Partial<AppSettingsSnapshot["claudeAuth"]>) => {
|
|
681
|
-
if (appSettings?.setClaudeAuth) return await appSettings.setClaudeAuth(patch)
|
|
682
|
-
fallbackAppSettingsSnapshot = mergeAppSettingsPatch(
|
|
683
|
-
appSettings?.getSnapshot() ?? fallbackAppSettingsSnapshot,
|
|
684
|
-
{ claudeAuth: patch },
|
|
685
|
-
)
|
|
686
|
-
return fallbackAppSettingsSnapshot
|
|
687
|
-
},
|
|
688
|
-
createSubagent: async (input: Parameters<AppSettingsManager["createSubagent"]>[0]) => {
|
|
689
|
-
if (appSettings?.createSubagent) return await appSettings.createSubagent(input)
|
|
690
|
-
const snapshot = await resolvedAppSettings.writePatch({ subagents: { create: input } })
|
|
691
|
-
return snapshot.subagents[snapshot.subagents.length - 1] ?? { code: "NOT_FOUND" as const, message: "Created subagent not found" }
|
|
692
|
-
},
|
|
693
|
-
updateSubagent: async (id: string, patch: Parameters<AppSettingsManager["updateSubagent"]>[1]) => {
|
|
694
|
-
if (appSettings?.updateSubagent) return await appSettings.updateSubagent(id, patch)
|
|
695
|
-
const snapshot = await resolvedAppSettings.writePatch({ subagents: { update: { id, patch } } })
|
|
696
|
-
return snapshot.subagents.find((subagent) => subagent.id === id) ?? { code: "NOT_FOUND" as const, message: `Subagent ${id} not found` }
|
|
697
|
-
},
|
|
698
|
-
deleteSubagent: async (id: string) => {
|
|
699
|
-
if (appSettings?.deleteSubagent) return await appSettings.deleteSubagent(id)
|
|
700
|
-
await resolvedAppSettings.writePatch({ subagents: { delete: { id } } })
|
|
701
|
-
},
|
|
702
|
-
onChange: (listener: (snapshot: AppSettingsSnapshot) => void) => appSettings?.onChange?.(listener) ?? (() => {}),
|
|
703
|
-
}
|
|
123
|
+
const resolvedDiffStore = diffStore ?? buildFallbackDiffStore()
|
|
124
|
+
const resolvedLlmProvider = llmProvider ?? buildFallbackLlmProvider()
|
|
125
|
+
const resolvedAppSettings = buildResolvedAppSettings(appSettings)
|
|
704
126
|
const resolvedAnalytics = analytics ?? NoopAnalyticsReporter
|
|
705
127
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
for (const socket of sockets) {
|
|
721
|
-
for (const chatId of socket.data.protectedDraftChatIds ?? []) {
|
|
722
|
-
protectedChatIds.add(chatId)
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
for (const socket of extraSockets ?? []) {
|
|
727
|
-
for (const chatId of socket.data.protectedDraftChatIds ?? []) {
|
|
728
|
-
protectedChatIds.add(chatId)
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
return protectedChatIds
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
async function maybePruneStaleEmptyChats(extraSockets?: Iterable<ServerWebSocket<ClientState>>) {
|
|
736
|
-
const startedAt = performance.now()
|
|
737
|
-
const activeChatIds = getProtectedChatIds()
|
|
738
|
-
const protectedDraftChatIds = getProtectedDraftChatIds(extraSockets)
|
|
739
|
-
const prunedChatIds = await store.pruneStaleEmptyChats?.({
|
|
740
|
-
activeChatIds,
|
|
741
|
-
protectedChatIds: protectedDraftChatIds,
|
|
742
|
-
})
|
|
743
|
-
if (isSendToStartingProfilingEnabled()) {
|
|
744
|
-
log.info("[kanna/send->starting][server]", JSON.stringify({
|
|
745
|
-
stage: "ws.prune_stale_empty_chats",
|
|
746
|
-
elapsedMs: Number((performance.now() - startedAt).toFixed(1)),
|
|
747
|
-
activeChatCount: activeChatIds.size,
|
|
748
|
-
protectedDraftChatCount: protectedDraftChatIds.size,
|
|
749
|
-
prunedCount: prunedChatIds?.length ?? 0,
|
|
750
|
-
totalChatCount: store.state.chatsById.size,
|
|
751
|
-
totalProjectCount: store.state.projectsById.size,
|
|
752
|
-
}))
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
function shouldIncludeTopic(topic: SubscriptionTopic, filter?: SnapshotBroadcastFilter) {
|
|
757
|
-
if (!filter) {
|
|
758
|
-
return true
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
if (topic.type === "sidebar") {
|
|
762
|
-
return Boolean(filter.includeSidebar)
|
|
763
|
-
}
|
|
764
|
-
if (topic.type === "local-projects") {
|
|
765
|
-
return Boolean(filter.includeLocalProjects)
|
|
766
|
-
}
|
|
767
|
-
if (topic.type === "update") {
|
|
768
|
-
return Boolean(filter.includeUpdate)
|
|
769
|
-
}
|
|
770
|
-
if (topic.type === "keybindings") {
|
|
771
|
-
return Boolean(filter.includeKeybindings)
|
|
772
|
-
}
|
|
773
|
-
if (topic.type === "app-settings") {
|
|
774
|
-
return Boolean(filter.includeAppSettings)
|
|
775
|
-
}
|
|
776
|
-
if (topic.type === "push-config") {
|
|
777
|
-
return Boolean(filter.includePushConfig)
|
|
778
|
-
}
|
|
779
|
-
if (topic.type === "chat") {
|
|
780
|
-
return filter.chatIds?.has(topic.chatId) ?? false
|
|
781
|
-
}
|
|
782
|
-
if (topic.type === "project-git") {
|
|
783
|
-
return filter.projectIds?.has(topic.projectId) ?? false
|
|
784
|
-
}
|
|
785
|
-
if (topic.type === "terminal") {
|
|
786
|
-
return filter.terminalIds?.has(topic.terminalId) ?? false
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
return true
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
function getSidebarSnapshotCacheEntry(cache?: SnapshotComputationCache) {
|
|
793
|
-
if (cache?.sidebar) {
|
|
794
|
-
return cache.sidebar
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
const startedAt = performance.now()
|
|
798
|
-
const data = deriveSidebarData(store.state, agent.getActiveStatuses(), {
|
|
799
|
-
sidebarProjectOrder: getSidebarProjectOrder(store),
|
|
800
|
-
drainingChatIds: agent.getDrainingChatIds(),
|
|
801
|
-
claudeSessionStates: agent.getClaudeSessionStates?.(),
|
|
802
|
-
})
|
|
803
|
-
const observed = data.projectGroups.flatMap((group) =>
|
|
804
|
-
group.chats.map((chat) => ({
|
|
805
|
-
chatId: chat.chatId,
|
|
806
|
-
projectLocalPath: group.localPath,
|
|
807
|
-
projectTitle: group.localPath.split("/").filter(Boolean).pop() ?? group.localPath,
|
|
808
|
-
chatTitle: chat.title,
|
|
809
|
-
status: chat.status,
|
|
810
|
-
}))
|
|
811
|
-
)
|
|
812
|
-
void pushManager.observeStatuses(observed).catch((error) => {
|
|
813
|
-
log.warn("[kanna/push] observeStatuses failed", { error })
|
|
814
|
-
})
|
|
815
|
-
if (isSendToStartingProfilingEnabled()) {
|
|
816
|
-
const totalChats = data.projectGroups.reduce((count, group) => count + group.chats.length, 0)
|
|
817
|
-
log.info("[kanna/send->starting][server]", JSON.stringify({
|
|
818
|
-
stage: "ws.sidebar_snapshot_built",
|
|
819
|
-
elapsedMs: Number((performance.now() - startedAt).toFixed(1)),
|
|
820
|
-
projectGroupCount: data.projectGroups.length,
|
|
821
|
-
chatCount: totalChats,
|
|
822
|
-
totalChatCount: store.state.chatsById.size,
|
|
823
|
-
totalProjectCount: store.state.projectsById.size,
|
|
824
|
-
}))
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
const sidebar = {
|
|
828
|
-
data,
|
|
829
|
-
signature: JSON.stringify({
|
|
830
|
-
type: "sidebar" as const,
|
|
831
|
-
data,
|
|
832
|
-
}),
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
if (cache) {
|
|
836
|
-
cache.sidebar = sidebar
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
return sidebar
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
function createEnvelope(
|
|
843
|
-
id: string,
|
|
844
|
-
topic: SubscriptionTopic,
|
|
845
|
-
cache?: SnapshotComputationCache,
|
|
846
|
-
connection?: ServerWebSocket<ClientState>,
|
|
847
|
-
): ServerEnvelope {
|
|
848
|
-
if (topic.type === "sidebar") {
|
|
849
|
-
const sidebar = getSidebarSnapshotCacheEntry(cache)
|
|
850
|
-
return {
|
|
851
|
-
v: PROTOCOL_VERSION,
|
|
852
|
-
type: "snapshot",
|
|
853
|
-
id,
|
|
854
|
-
snapshot: {
|
|
855
|
-
type: "sidebar",
|
|
856
|
-
data: sidebar.data,
|
|
857
|
-
},
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
if (topic.type === "local-projects") {
|
|
862
|
-
const discoveredProjects = getDiscoveredProjects()
|
|
863
|
-
const data = deriveLocalProjectsSnapshot(store.state, discoveredProjects, machineDisplayName, os.homedir())
|
|
864
|
-
|
|
865
|
-
return {
|
|
866
|
-
v: PROTOCOL_VERSION,
|
|
867
|
-
type: "snapshot",
|
|
868
|
-
id,
|
|
869
|
-
snapshot: {
|
|
870
|
-
type: "local-projects",
|
|
871
|
-
data,
|
|
872
|
-
},
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
if (topic.type === "keybindings") {
|
|
877
|
-
return {
|
|
878
|
-
v: PROTOCOL_VERSION,
|
|
879
|
-
type: "snapshot",
|
|
880
|
-
id,
|
|
881
|
-
snapshot: {
|
|
882
|
-
type: "keybindings",
|
|
883
|
-
data: keybindings.getSnapshot(),
|
|
884
|
-
},
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
if (topic.type === "app-settings") {
|
|
889
|
-
return {
|
|
890
|
-
v: PROTOCOL_VERSION,
|
|
891
|
-
type: "snapshot",
|
|
892
|
-
id,
|
|
893
|
-
snapshot: {
|
|
894
|
-
type: "app-settings",
|
|
895
|
-
data: resolvedAppSettings.getSnapshot(),
|
|
896
|
-
},
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
if (topic.type === "push-config") {
|
|
901
|
-
return {
|
|
902
|
-
v: PROTOCOL_VERSION,
|
|
903
|
-
type: "snapshot",
|
|
904
|
-
id,
|
|
905
|
-
snapshot: {
|
|
906
|
-
type: "push-config",
|
|
907
|
-
data: pushManager.getConfigSnapshot(connection?.data.pushDeviceId ?? null),
|
|
908
|
-
},
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
if (topic.type === "update") {
|
|
913
|
-
return {
|
|
914
|
-
v: PROTOCOL_VERSION,
|
|
915
|
-
type: "snapshot",
|
|
916
|
-
id,
|
|
917
|
-
snapshot: {
|
|
918
|
-
type: "update",
|
|
919
|
-
data: updateManager?.getSnapshot() ?? {
|
|
920
|
-
currentVersion: "unknown",
|
|
921
|
-
latestVersion: null,
|
|
922
|
-
status: "idle",
|
|
923
|
-
updateAvailable: false,
|
|
924
|
-
lastCheckedAt: null,
|
|
925
|
-
error: null,
|
|
926
|
-
installAction: "restart",
|
|
927
|
-
reloadRequestedAt: null,
|
|
928
|
-
},
|
|
929
|
-
},
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
if (topic.type === "terminal") {
|
|
934
|
-
return {
|
|
935
|
-
v: PROTOCOL_VERSION,
|
|
936
|
-
type: "snapshot",
|
|
937
|
-
id,
|
|
938
|
-
snapshot: {
|
|
939
|
-
type: "terminal",
|
|
940
|
-
data: terminals.getSnapshot(topic.terminalId),
|
|
941
|
-
},
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
if (topic.type === "project-git") {
|
|
946
|
-
return {
|
|
947
|
-
v: PROTOCOL_VERSION,
|
|
948
|
-
type: "snapshot",
|
|
949
|
-
id,
|
|
950
|
-
snapshot: {
|
|
951
|
-
type: "project-git",
|
|
952
|
-
data: store.getProject(topic.projectId)
|
|
953
|
-
? resolvedDiffStore.getProjectSnapshot(topic.projectId)
|
|
954
|
-
: null,
|
|
955
|
-
},
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
if (topic.type === "pty-instances") {
|
|
960
|
-
return {
|
|
961
|
-
v: PROTOCOL_VERSION,
|
|
962
|
-
type: "snapshot",
|
|
963
|
-
id,
|
|
964
|
-
snapshot: {
|
|
965
|
-
type: "pty-instances",
|
|
966
|
-
data: { instances: ptyInstances?.snapshot() ?? [] },
|
|
967
|
-
},
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
if (topic.type === "workflows") {
|
|
972
|
-
return {
|
|
973
|
-
v: PROTOCOL_VERSION,
|
|
974
|
-
type: "snapshot",
|
|
975
|
-
id,
|
|
976
|
-
snapshot: {
|
|
977
|
-
type: "workflows",
|
|
978
|
-
data: { chatId: topic.chatId, runs: workflowRegistry?.snapshot(topic.chatId) ?? [] },
|
|
979
|
-
},
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
if (topic.type === "orch-runs") {
|
|
984
|
-
return {
|
|
985
|
-
v: PROTOCOL_VERSION,
|
|
986
|
-
type: "snapshot",
|
|
987
|
-
id,
|
|
988
|
-
snapshot: {
|
|
989
|
-
type: "orch-runs",
|
|
990
|
-
data: { runs: store.getOrchRuns().map(toOrchRunSummary) },
|
|
991
|
-
},
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
return {
|
|
996
|
-
v: PROTOCOL_VERSION,
|
|
997
|
-
type: "snapshot",
|
|
998
|
-
id,
|
|
999
|
-
snapshot: {
|
|
1000
|
-
type: "chat",
|
|
1001
|
-
data: deriveChatSnapshot(
|
|
1002
|
-
store.state,
|
|
1003
|
-
agent.getActiveStatuses(),
|
|
1004
|
-
agent.getDrainingChatIds(),
|
|
1005
|
-
agent.getSlashCommandsLoadingChatIds(),
|
|
1006
|
-
topic.chatId,
|
|
1007
|
-
(chatId) => store.getRecentChatHistory(chatId, topic.recentLimit ?? DEFAULT_CHAT_RECENT_LIMIT),
|
|
1008
|
-
(chatId) => store.getTunnelEvents(chatId),
|
|
1009
|
-
agent.getWaitStartedAtByChatId(),
|
|
1010
|
-
Date.now(),
|
|
1011
|
-
agent.getClaudeSessionStates?.() ?? new Map(),
|
|
1012
|
-
appSettings?.getSnapshot().customModels ?? [],
|
|
1013
|
-
),
|
|
1014
|
-
},
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
// timings.derivedAtMs = Date.now() on every call, making every snapshot unique
|
|
1019
|
-
// and defeating signature-based dedup. Strip timings from the signature so that
|
|
1020
|
-
// idle/finished chats are only sent once instead of on every broadcastSnapshots call.
|
|
1021
|
-
function getStableChatSnapshotSignature(snapshot: Extract<ServerEnvelope, { type: "snapshot" }>["snapshot"]): string {
|
|
1022
|
-
if (snapshot.type === "chat" && snapshot.data?.runtime) {
|
|
1023
|
-
const { timings: _t, ...stableRuntime } = snapshot.data.runtime
|
|
1024
|
-
return JSON.stringify({ type: snapshot.type, data: { ...snapshot.data, runtime: stableRuntime } })
|
|
1025
|
-
}
|
|
1026
|
-
return JSON.stringify(snapshot)
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
async function pushSnapshots(
|
|
1030
|
-
ws: ServerWebSocket<ClientState>,
|
|
1031
|
-
options?: { skipPrune?: boolean; filter?: SnapshotBroadcastFilter; cache?: SnapshotComputationCache }
|
|
1032
|
-
) {
|
|
1033
|
-
const pushStartedAt = performance.now()
|
|
1034
|
-
if (!options?.skipPrune) {
|
|
1035
|
-
await maybePruneStaleEmptyChats([ws])
|
|
1036
|
-
}
|
|
1037
|
-
const snapshotSignatures = ensureSnapshotSignatures(ws)
|
|
1038
|
-
let sentCount = 0
|
|
1039
|
-
let skippedCount = 0
|
|
1040
|
-
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
1041
|
-
if (!shouldIncludeTopic(topic, options?.filter)) {
|
|
1042
|
-
continue
|
|
1043
|
-
}
|
|
1044
|
-
const envelopeStartedAt = performance.now()
|
|
1045
|
-
const envelope = createEnvelope(id, topic, options?.cache, ws)
|
|
1046
|
-
const createdAt = performance.now()
|
|
1047
|
-
if (envelope.type !== "snapshot") continue
|
|
1048
|
-
const signature = topic.type === "sidebar"
|
|
1049
|
-
? getSidebarSnapshotCacheEntry(options?.cache).signature
|
|
1050
|
-
: getStableChatSnapshotSignature(envelope.snapshot)
|
|
1051
|
-
const signatureReadyAt = topic.type === "sidebar" ? createdAt : performance.now()
|
|
1052
|
-
if (snapshotSignatures.get(id) === signature) {
|
|
1053
|
-
skippedCount += 1
|
|
1054
|
-
continue
|
|
1055
|
-
}
|
|
1056
|
-
snapshotSignatures.set(id, signature)
|
|
1057
|
-
if (topic.type === "chat" && envelope.snapshot.type === "chat" && envelope.snapshot.data?.runtime.status === "starting") {
|
|
1058
|
-
const profile = agent.getActiveTurnProfile(topic.chatId)
|
|
1059
|
-
logSendToStartingProfile(profile?.traceId, profile?.startedAt, "ws.snapshot_sent", {
|
|
1060
|
-
chatId: topic.chatId,
|
|
1061
|
-
status: envelope.snapshot.data.runtime.status,
|
|
1062
|
-
messageCount: envelope.snapshot.data.messages.length,
|
|
1063
|
-
buildMs: Number((createdAt - envelopeStartedAt).toFixed(1)),
|
|
1064
|
-
signatureMs: Number((signatureReadyAt - createdAt).toFixed(1)),
|
|
1065
|
-
signatureBytes: signature.length,
|
|
1066
|
-
})
|
|
1067
|
-
}
|
|
1068
|
-
const payloadBytes = send(ws, envelope)
|
|
1069
|
-
sentCount += 1
|
|
1070
|
-
if (topic.type === "chat" && envelope.snapshot.type === "chat" && envelope.snapshot.data?.runtime.status === "starting") {
|
|
1071
|
-
const profile = agent.getActiveTurnProfile(topic.chatId)
|
|
1072
|
-
logSendToStartingProfile(profile?.traceId, profile?.startedAt, "ws.snapshot_send_completed", {
|
|
1073
|
-
chatId: topic.chatId,
|
|
1074
|
-
payloadBytes,
|
|
1075
|
-
})
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
if (isSendToStartingProfilingEnabled()) {
|
|
1079
|
-
log.info("[kanna/send->starting][server]", JSON.stringify({
|
|
1080
|
-
stage: "ws.push_snapshots_completed",
|
|
1081
|
-
elapsedMs: Number((performance.now() - pushStartedAt).toFixed(1)),
|
|
1082
|
-
skipPrune: Boolean(options?.skipPrune),
|
|
1083
|
-
sentCount,
|
|
1084
|
-
skippedCount,
|
|
1085
|
-
...countSubscriptionsByTopic(ws),
|
|
1086
|
-
}))
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
async function broadcastSnapshots() {
|
|
1091
|
-
const startedAt = performance.now()
|
|
1092
|
-
let socketCount = 0
|
|
1093
|
-
const cache: SnapshotComputationCache = {}
|
|
1094
|
-
for (const ws of sockets) {
|
|
1095
|
-
socketCount += 1
|
|
1096
|
-
await pushSnapshots(ws, { skipPrune: true, cache })
|
|
1097
|
-
}
|
|
1098
|
-
if (isSendToStartingProfilingEnabled()) {
|
|
1099
|
-
log.info("[kanna/send->starting][server]", JSON.stringify({
|
|
1100
|
-
stage: "ws.broadcast_snapshots_completed",
|
|
1101
|
-
elapsedMs: Number((performance.now() - startedAt).toFixed(1)),
|
|
1102
|
-
pruneMs: 0,
|
|
1103
|
-
socketCount,
|
|
1104
|
-
totalChatCount: store.state.chatsById.size,
|
|
1105
|
-
totalProjectCount: store.state.projectsById.size,
|
|
1106
|
-
}))
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
async function broadcastFilteredSnapshots(filter: SnapshotBroadcastFilter) {
|
|
1111
|
-
const startedAt = performance.now()
|
|
1112
|
-
let socketCount = 0
|
|
1113
|
-
const cache: SnapshotComputationCache = {}
|
|
1114
|
-
for (const ws of sockets) {
|
|
1115
|
-
socketCount += 1
|
|
1116
|
-
await pushSnapshots(ws, { skipPrune: true, filter, cache })
|
|
1117
|
-
}
|
|
1118
|
-
if (isSendToStartingProfilingEnabled()) {
|
|
1119
|
-
log.info("[kanna/send->starting][server]", JSON.stringify({
|
|
1120
|
-
stage: "ws.broadcast_filtered_snapshots_completed",
|
|
1121
|
-
elapsedMs: Number((performance.now() - startedAt).toFixed(1)),
|
|
1122
|
-
socketCount,
|
|
1123
|
-
includeSidebar: Boolean(filter.includeSidebar),
|
|
1124
|
-
chatCount: filter.chatIds?.size ?? 0,
|
|
1125
|
-
projectCount: filter.projectIds?.size ?? 0,
|
|
1126
|
-
}))
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
function scheduleBroadcast() {
|
|
1131
|
-
pendingBroadcastAll = true
|
|
1132
|
-
pendingBroadcastChatIds.clear()
|
|
1133
|
-
if (pendingBroadcastTimer) {
|
|
1134
|
-
return
|
|
1135
|
-
}
|
|
1136
|
-
pendingBroadcastTimer = setTimeout(() => {
|
|
1137
|
-
pendingBroadcastTimer = null
|
|
1138
|
-
const shouldBroadcastAll = pendingBroadcastAll
|
|
1139
|
-
const chatIds = new Set(pendingBroadcastChatIds)
|
|
1140
|
-
pendingBroadcastAll = false
|
|
1141
|
-
pendingBroadcastChatIds.clear()
|
|
1142
|
-
if (shouldBroadcastAll) {
|
|
1143
|
-
void broadcastSnapshots()
|
|
1144
|
-
return
|
|
1145
|
-
}
|
|
1146
|
-
if (chatIds.size > 0) {
|
|
1147
|
-
void broadcastFilteredSnapshots({
|
|
1148
|
-
includeSidebar: true,
|
|
1149
|
-
chatIds,
|
|
1150
|
-
})
|
|
1151
|
-
}
|
|
1152
|
-
}, 16)
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
function scheduleChatStateBroadcast(chatId: string) {
|
|
1156
|
-
if (!pendingBroadcastAll) {
|
|
1157
|
-
pendingBroadcastChatIds.add(chatId)
|
|
1158
|
-
}
|
|
1159
|
-
if (pendingBroadcastTimer) {
|
|
1160
|
-
return
|
|
1161
|
-
}
|
|
1162
|
-
pendingBroadcastTimer = setTimeout(() => {
|
|
1163
|
-
pendingBroadcastTimer = null
|
|
1164
|
-
const shouldBroadcastAll = pendingBroadcastAll
|
|
1165
|
-
const chatIds = new Set(pendingBroadcastChatIds)
|
|
1166
|
-
pendingBroadcastAll = false
|
|
1167
|
-
pendingBroadcastChatIds.clear()
|
|
1168
|
-
if (shouldBroadcastAll) {
|
|
1169
|
-
void broadcastSnapshots()
|
|
1170
|
-
return
|
|
1171
|
-
}
|
|
1172
|
-
if (chatIds.size > 0) {
|
|
1173
|
-
void broadcastFilteredSnapshots({
|
|
1174
|
-
includeSidebar: true,
|
|
1175
|
-
chatIds,
|
|
1176
|
-
})
|
|
1177
|
-
}
|
|
1178
|
-
}, 16)
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
async function broadcastChatAndSidebar(chatId: string) {
|
|
1182
|
-
await broadcastFilteredSnapshots({
|
|
1183
|
-
includeSidebar: true,
|
|
1184
|
-
chatIds: new Set([chatId]),
|
|
1185
|
-
})
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
async function broadcastChatStateImmediately(chatId: string) {
|
|
1189
|
-
await broadcastChatAndSidebar(chatId)
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
function broadcastError(message: string) {
|
|
1193
|
-
for (const ws of sockets) {
|
|
1194
|
-
send(ws, {
|
|
1195
|
-
v: PROTOCOL_VERSION,
|
|
1196
|
-
type: "error",
|
|
1197
|
-
message,
|
|
1198
|
-
})
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
function pushTerminalSnapshot(terminalId: string) {
|
|
1203
|
-
for (const ws of sockets) {
|
|
1204
|
-
const snapshotSignatures = ensureSnapshotSignatures(ws)
|
|
1205
|
-
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
1206
|
-
if (topic.type !== "terminal" || topic.terminalId !== terminalId) continue
|
|
1207
|
-
const envelope = createEnvelope(id, topic, undefined, ws)
|
|
1208
|
-
if (envelope.type !== "snapshot") continue
|
|
1209
|
-
const signature = JSON.stringify(envelope.snapshot)
|
|
1210
|
-
if (snapshotSignatures.get(id) === signature) continue
|
|
1211
|
-
snapshotSignatures.set(id, signature)
|
|
1212
|
-
send(ws, envelope)
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
function pushTerminalEvent(terminalId: string, event: Extract<ServerEnvelope, { type: "event" }>["event"]) {
|
|
1218
|
-
for (const ws of sockets) {
|
|
1219
|
-
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
1220
|
-
if (topic.type !== "terminal" || topic.terminalId !== terminalId) continue
|
|
1221
|
-
send(ws, {
|
|
1222
|
-
v: PROTOCOL_VERSION,
|
|
1223
|
-
type: "event",
|
|
1224
|
-
id,
|
|
1225
|
-
event,
|
|
1226
|
-
})
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
const disposeTerminalEvents = terminals.onEvent((event) => {
|
|
1232
|
-
pushTerminalEvent(event.terminalId, event)
|
|
1233
|
-
})
|
|
1234
|
-
|
|
1235
|
-
const disposeKeybindingEvents = keybindings.onChange(() => {
|
|
1236
|
-
for (const ws of sockets) {
|
|
1237
|
-
const snapshotSignatures = ensureSnapshotSignatures(ws)
|
|
1238
|
-
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
1239
|
-
if (topic.type !== "keybindings") continue
|
|
1240
|
-
const envelope = createEnvelope(id, topic, undefined, ws)
|
|
1241
|
-
if (envelope.type !== "snapshot") continue
|
|
1242
|
-
const signature = JSON.stringify(envelope.snapshot)
|
|
1243
|
-
if (snapshotSignatures.get(id) === signature) continue
|
|
1244
|
-
snapshotSignatures.set(id, signature)
|
|
1245
|
-
send(ws, envelope)
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
})
|
|
1249
|
-
|
|
1250
|
-
const disposeAppSettingsEvents = resolvedAppSettings.onChange(() => {
|
|
1251
|
-
for (const ws of sockets) {
|
|
1252
|
-
const snapshotSignatures = ensureSnapshotSignatures(ws)
|
|
1253
|
-
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
1254
|
-
if (topic.type !== "app-settings") continue
|
|
1255
|
-
const envelope = createEnvelope(id, topic, undefined, ws)
|
|
1256
|
-
if (envelope.type !== "snapshot") continue
|
|
1257
|
-
const signature = JSON.stringify(envelope.snapshot)
|
|
1258
|
-
if (snapshotSignatures.get(id) === signature) continue
|
|
1259
|
-
snapshotSignatures.set(id, signature)
|
|
1260
|
-
send(ws, envelope)
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
128
|
+
const envelopeBuilder = createEnvelopeBuilder({
|
|
129
|
+
store,
|
|
130
|
+
agent,
|
|
131
|
+
resolvedAppSettings,
|
|
132
|
+
keybindings,
|
|
133
|
+
resolvedDiffStore,
|
|
134
|
+
ptyInstances,
|
|
135
|
+
workflowRegistry,
|
|
136
|
+
machineDisplayName,
|
|
137
|
+
updateManager,
|
|
138
|
+
getDiscoveredProjects,
|
|
139
|
+
terminals,
|
|
140
|
+
pushManager,
|
|
1263
141
|
})
|
|
1264
142
|
|
|
1265
|
-
const
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
}) ?? (() => {})
|
|
1279
|
-
|
|
1280
|
-
function pushPtyInstancesEvent(event: PtyInstancesEvent) {
|
|
1281
|
-
for (const ws of sockets) {
|
|
1282
|
-
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
1283
|
-
if (topic.type !== "pty-instances") continue
|
|
1284
|
-
send(ws, { v: PROTOCOL_VERSION, type: "event", id, event })
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
const disposePtyInstances: () => void = ptyInstances?.subscribe((delta: PtyInstanceDelta) => {
|
|
1290
|
-
if (delta.type === "added") {
|
|
1291
|
-
pushPtyInstancesEvent({ type: "pty-instances.added", instance: delta.instance })
|
|
1292
|
-
} else if (delta.type === "updated") {
|
|
1293
|
-
pushPtyInstancesEvent({ type: "pty-instances.updated", instance: delta.instance })
|
|
1294
|
-
} else {
|
|
1295
|
-
pushPtyInstancesEvent({ type: "pty-instances.removed", chatId: delta.chatId })
|
|
1296
|
-
}
|
|
1297
|
-
}) ?? (() => {})
|
|
1298
|
-
|
|
1299
|
-
const disposeWorkflows: () => void = workflowRegistry?.subscribe((chatId) => {
|
|
1300
|
-
for (const ws of sockets) {
|
|
1301
|
-
const snapshotSignatures = ensureSnapshotSignatures(ws)
|
|
1302
|
-
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
1303
|
-
if (topic.type !== "workflows" || topic.chatId !== chatId) continue
|
|
1304
|
-
const envelope = createEnvelope(id, topic, undefined, ws)
|
|
1305
|
-
if (envelope.type !== "snapshot") continue
|
|
1306
|
-
const signature = JSON.stringify(envelope.snapshot)
|
|
1307
|
-
if (snapshotSignatures.get(id) === signature) continue
|
|
1308
|
-
snapshotSignatures.set(id, signature)
|
|
1309
|
-
send(ws, envelope)
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
}) ?? (() => {})
|
|
1313
|
-
|
|
1314
|
-
const pushOrchRuns = () => {
|
|
1315
|
-
for (const ws of sockets) {
|
|
1316
|
-
const snapshotSignatures = ensureSnapshotSignatures(ws)
|
|
1317
|
-
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
1318
|
-
if (topic.type !== "orch-runs") continue
|
|
1319
|
-
const envelope = createEnvelope(id, topic, undefined, ws)
|
|
1320
|
-
if (envelope.type !== "snapshot") continue
|
|
1321
|
-
const signature = JSON.stringify(envelope.snapshot)
|
|
1322
|
-
if (snapshotSignatures.get(id) === signature) continue
|
|
1323
|
-
snapshotSignatures.set(id, signature)
|
|
1324
|
-
send(ws, envelope)
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
// Optional like the registry subscriptions above: partial store fakes in
|
|
1329
|
-
// tests may not implement it. The real EventStore always does.
|
|
1330
|
-
const disposeOrchRuns: () => void = typeof store.subscribeOrchRuns === "function"
|
|
1331
|
-
? store.subscribeOrchRuns(pushOrchRuns)
|
|
1332
|
-
: () => {}
|
|
1333
|
-
|
|
1334
|
-
agent.setBackgroundErrorReporter?.(broadcastError)
|
|
143
|
+
const broadcast = new BroadcastManager({
|
|
144
|
+
agent,
|
|
145
|
+
store,
|
|
146
|
+
terminals,
|
|
147
|
+
keybindings,
|
|
148
|
+
resolvedAppSettings,
|
|
149
|
+
updateManager,
|
|
150
|
+
ptyInstances,
|
|
151
|
+
workflowRegistry,
|
|
152
|
+
envelopeBuilder,
|
|
153
|
+
})
|
|
1335
154
|
|
|
1336
155
|
function resolveChatProject(chatId: string) {
|
|
1337
156
|
const chat = store.getChat(chatId)
|
|
@@ -1345,948 +164,298 @@ export function createWsRouter({
|
|
|
1345
164
|
const { command, id } = message
|
|
1346
165
|
try {
|
|
1347
166
|
switch (command.type) {
|
|
1348
|
-
case "
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
case "
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
case "
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
return
|
|
1380
|
-
}
|
|
1381
|
-
case "update.reload": {
|
|
1382
|
-
if (!updateManager) {
|
|
1383
|
-
throw new Error("Update manager unavailable.")
|
|
1384
|
-
}
|
|
1385
|
-
const result = await updateManager.forceReload()
|
|
1386
|
-
send(ws, {
|
|
1387
|
-
v: PROTOCOL_VERSION,
|
|
1388
|
-
type: "ack",
|
|
1389
|
-
id,
|
|
1390
|
-
result,
|
|
1391
|
-
})
|
|
1392
|
-
return
|
|
1393
|
-
}
|
|
1394
|
-
case "settings.readKeybindings": {
|
|
1395
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: keybindings.getSnapshot() })
|
|
1396
|
-
return
|
|
1397
|
-
}
|
|
1398
|
-
case "settings.writeKeybindings": {
|
|
1399
|
-
const snapshot = await keybindings.write(command.bindings)
|
|
1400
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
|
|
1401
|
-
return
|
|
1402
|
-
}
|
|
1403
|
-
case "settings.readAppSettings": {
|
|
1404
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: resolvedAppSettings.getSnapshot() })
|
|
1405
|
-
return
|
|
1406
|
-
}
|
|
1407
|
-
case "settings.writeAppSettings": {
|
|
1408
|
-
const previousAnalyticsEnabled = resolvedAppSettings.getSnapshot().analyticsEnabled
|
|
1409
|
-
if (previousAnalyticsEnabled && !command.analyticsEnabled) {
|
|
1410
|
-
resolvedAnalytics.track("analytics_disabled")
|
|
1411
|
-
}
|
|
1412
|
-
const snapshot = await resolvedAppSettings.write({ analyticsEnabled: command.analyticsEnabled })
|
|
1413
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
|
|
1414
|
-
if (!previousAnalyticsEnabled && command.analyticsEnabled) {
|
|
1415
|
-
resolvedAnalytics.track("analytics_enabled")
|
|
1416
|
-
}
|
|
1417
|
-
return
|
|
1418
|
-
}
|
|
1419
|
-
case "appSettings.setCloudflareTunnel": {
|
|
1420
|
-
await resolvedAppSettings.setCloudflareTunnel(command.patch)
|
|
1421
|
-
const snapshot = resolvedAppSettings.getSnapshot()
|
|
1422
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
|
|
1423
|
-
return
|
|
1424
|
-
}
|
|
1425
|
-
case "appSettings.setClaudeAuth": {
|
|
1426
|
-
await resolvedAppSettings.setClaudeAuth(command.patch)
|
|
1427
|
-
const snapshot = resolvedAppSettings.getSnapshot()
|
|
1428
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
|
|
1429
|
-
return
|
|
1430
|
-
}
|
|
1431
|
-
case "appSettings.testOAuthToken": {
|
|
1432
|
-
const result = await testOAuthToken(command.token)
|
|
1433
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
1434
|
-
return
|
|
1435
|
-
}
|
|
1436
|
-
case "settings.writeAppSettingsPatch": {
|
|
1437
|
-
const previousAnalyticsEnabled = resolvedAppSettings.getSnapshot().analyticsEnabled
|
|
1438
|
-
const snapshot = await resolvedAppSettings.writePatch(command.patch)
|
|
1439
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
|
|
1440
|
-
|
|
1441
|
-
// Fire-and-forget auto-test for newly created or updated MCP server.
|
|
1442
|
-
const targetId = (() => {
|
|
1443
|
-
const ops = command.patch.customMcpServers
|
|
1444
|
-
if (!ops) return null
|
|
1445
|
-
if (ops.update) return ops.update.id
|
|
1446
|
-
if (ops.create) {
|
|
1447
|
-
// The created entry is the one with no prior match by name —
|
|
1448
|
-
// simplest: pick the entry with the latest createdAt.
|
|
1449
|
-
const list = snapshot.customMcpServers
|
|
1450
|
-
if (list.length === 0) return null
|
|
1451
|
-
return list.reduce((latest, e) => (e.createdAt > latest.createdAt ? e : latest), list[0]!).id
|
|
1452
|
-
}
|
|
1453
|
-
return null
|
|
1454
|
-
})()
|
|
1455
|
-
if (targetId) {
|
|
1456
|
-
void runMcpAutoTest(targetId, resolvedAppSettings)
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
if (command.patch.analyticsEnabled !== undefined && previousAnalyticsEnabled && !snapshot.analyticsEnabled) {
|
|
1460
|
-
resolvedAnalytics.track("analytics_disabled")
|
|
1461
|
-
}
|
|
1462
|
-
if (command.patch.analyticsEnabled !== undefined && !previousAnalyticsEnabled && snapshot.analyticsEnabled) {
|
|
1463
|
-
resolvedAnalytics.track("analytics_enabled")
|
|
1464
|
-
}
|
|
1465
|
-
return
|
|
1466
|
-
}
|
|
1467
|
-
case "subagent.create": {
|
|
1468
|
-
const result = await resolvedAppSettings.createSubagent(command.input)
|
|
1469
|
-
send(ws, {
|
|
1470
|
-
v: PROTOCOL_VERSION,
|
|
1471
|
-
type: "ack",
|
|
1472
|
-
id,
|
|
1473
|
-
result: isSubagentValidationError(result)
|
|
1474
|
-
? { ok: false, error: result }
|
|
1475
|
-
: { ok: true, subagent: result },
|
|
1476
|
-
})
|
|
1477
|
-
return
|
|
1478
|
-
}
|
|
1479
|
-
case "subagent.update": {
|
|
1480
|
-
const result = await resolvedAppSettings.updateSubagent(command.id, command.patch)
|
|
1481
|
-
send(ws, {
|
|
1482
|
-
v: PROTOCOL_VERSION,
|
|
1483
|
-
type: "ack",
|
|
1484
|
-
id,
|
|
1485
|
-
result: isSubagentValidationError(result)
|
|
1486
|
-
? { ok: false, error: result }
|
|
1487
|
-
: { ok: true, subagent: result },
|
|
1488
|
-
})
|
|
1489
|
-
return
|
|
1490
|
-
}
|
|
1491
|
-
case "subagent.delete": {
|
|
1492
|
-
await resolvedAppSettings.deleteSubagent(command.id)
|
|
1493
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { ok: true } })
|
|
1494
|
-
return
|
|
1495
|
-
}
|
|
1496
|
-
case "settings.testMcpServer": {
|
|
1497
|
-
const snapshot = resolvedAppSettings.getSnapshot()
|
|
1498
|
-
const entry = snapshot.customMcpServers.find((s) => s.id === command.id)
|
|
1499
|
-
if (!entry) {
|
|
1500
|
-
send(ws, {
|
|
1501
|
-
v: PROTOCOL_VERSION,
|
|
1502
|
-
type: "ack",
|
|
1503
|
-
id,
|
|
1504
|
-
result: {
|
|
1505
|
-
ok: false,
|
|
1506
|
-
message: "MCP server not found",
|
|
1507
|
-
lastTest: { status: "error", testedAt: new Date().toISOString(), message: "not found" } as const,
|
|
1508
|
-
},
|
|
1509
|
-
})
|
|
1510
|
-
return
|
|
1511
|
-
}
|
|
1512
|
-
// Mark pending so the UI sees a spinner while we connect.
|
|
1513
|
-
await resolvedAppSettings.writePatch({
|
|
1514
|
-
customMcpServers: {
|
|
1515
|
-
setTestResult: { id: entry.id, result: { status: "pending", startedAt: new Date().toISOString() } },
|
|
167
|
+
case "settings.readKeybindings":
|
|
168
|
+
case "settings.writeKeybindings":
|
|
169
|
+
case "settings.readAppSettings":
|
|
170
|
+
case "settings.writeAppSettings":
|
|
171
|
+
case "appSettings.setCloudflareTunnel":
|
|
172
|
+
case "appSettings.setClaudeAuth":
|
|
173
|
+
case "appSettings.testOAuthToken":
|
|
174
|
+
case "settings.writeAppSettingsPatch":
|
|
175
|
+
case "subagent.create":
|
|
176
|
+
case "subagent.update":
|
|
177
|
+
case "subagent.delete":
|
|
178
|
+
case "settings.testMcpServer":
|
|
179
|
+
case "settings.startMcpOAuth":
|
|
180
|
+
case "settings.completeMcpOAuth":
|
|
181
|
+
case "settings.readLlmProvider":
|
|
182
|
+
case "settings.listOpenRouterModels":
|
|
183
|
+
case "settings.getChangelog":
|
|
184
|
+
case "settings.writeLlmProvider":
|
|
185
|
+
case "settings.validateLlmProvider":
|
|
186
|
+
case "skills.search":
|
|
187
|
+
case "skills.install":
|
|
188
|
+
case "skills.uninstall":
|
|
189
|
+
case "skills.listInstalled": {
|
|
190
|
+
await handleSettingsCommand(
|
|
191
|
+
{
|
|
192
|
+
keybindings,
|
|
193
|
+
resolvedAppSettings,
|
|
194
|
+
resolvedAnalytics,
|
|
195
|
+
resolvedLlmProvider,
|
|
196
|
+
listOpenRouterModels,
|
|
197
|
+
send: (envelope) => send(ws, envelope),
|
|
1516
198
|
},
|
|
1517
|
-
|
|
1518
|
-
const testBearer = await resolveMcpTestBearer(entry, resolvedAppSettings)
|
|
1519
|
-
const lastTest = await validateMcpServer(entry, testBearer ? { bearer: testBearer } : {})
|
|
1520
|
-
await resolvedAppSettings.writePatch({
|
|
1521
|
-
customMcpServers: { setTestResult: { id: entry.id, result: lastTest } },
|
|
1522
|
-
})
|
|
1523
|
-
send(ws, {
|
|
1524
|
-
v: PROTOCOL_VERSION,
|
|
1525
|
-
type: "ack",
|
|
199
|
+
command,
|
|
1526
200
|
id,
|
|
1527
|
-
|
|
1528
|
-
ok: lastTest.status === "ok",
|
|
1529
|
-
message: lastTest.status === "error" ? lastTest.message : undefined,
|
|
1530
|
-
lastTest,
|
|
1531
|
-
},
|
|
1532
|
-
})
|
|
1533
|
-
return
|
|
1534
|
-
}
|
|
1535
|
-
case "settings.startMcpOAuth": {
|
|
1536
|
-
const snapshot = resolvedAppSettings.getSnapshot()
|
|
1537
|
-
const entry = snapshot.customMcpServers.find((s) => s.id === command.id)
|
|
1538
|
-
if (!entry || entry.transport === "stdio") {
|
|
1539
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { ok: false, error: "not found or unsupported transport" } })
|
|
1540
|
-
return
|
|
1541
|
-
}
|
|
1542
|
-
try {
|
|
1543
|
-
const result = await startMcpOAuth(entry, {
|
|
1544
|
-
persist: (oauth) => void resolvedAppSettings.writePatch({ customMcpServers: { setOAuthState: { id: entry.id, oauth } } }),
|
|
1545
|
-
})
|
|
1546
|
-
send(ws, {
|
|
1547
|
-
v: PROTOCOL_VERSION, type: "ack", id,
|
|
1548
|
-
result: result.kind === "authorizationUrl"
|
|
1549
|
-
? { ok: true, authorizationUrl: result.authorizationUrl }
|
|
1550
|
-
: { ok: true, alreadyAuthenticated: true },
|
|
1551
|
-
})
|
|
1552
|
-
} catch (err) {
|
|
1553
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { ok: false, error: err instanceof Error ? err.message : "oauth start failed" } })
|
|
1554
|
-
}
|
|
1555
|
-
return
|
|
1556
|
-
}
|
|
1557
|
-
case "settings.completeMcpOAuth": {
|
|
1558
|
-
const snapshot = resolvedAppSettings.getSnapshot()
|
|
1559
|
-
const entry = snapshot.customMcpServers.find((s) => s.id === command.id)
|
|
1560
|
-
if (!entry || entry.transport === "stdio") {
|
|
1561
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { ok: false, error: "not found" } })
|
|
1562
|
-
return
|
|
1563
|
-
}
|
|
1564
|
-
try {
|
|
1565
|
-
const result = await completeMcpOAuth(entry, command.callbackUrl, {
|
|
1566
|
-
persist: (oauth) => void resolvedAppSettings.writePatch({ customMcpServers: { setOAuthState: { id: entry.id, oauth } } }),
|
|
1567
|
-
listTools: async (_serverUrl, accessToken) => {
|
|
1568
|
-
const r = await validateMcpServer(entry, { bearer: accessToken })
|
|
1569
|
-
return r.status === "ok" ? r.toolCount : 0
|
|
1570
|
-
},
|
|
1571
|
-
})
|
|
1572
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { ok: true, testResult: result } })
|
|
1573
|
-
} catch (err) {
|
|
1574
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { ok: false, error: err instanceof Error ? err.message : "oauth complete failed" } })
|
|
1575
|
-
}
|
|
1576
|
-
return
|
|
1577
|
-
}
|
|
1578
|
-
case "settings.readLlmProvider": {
|
|
1579
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: await resolvedLlmProvider.read() })
|
|
1580
|
-
return
|
|
1581
|
-
}
|
|
1582
|
-
case "settings.listOpenRouterModels": {
|
|
1583
|
-
const models = listOpenRouterModels ? await listOpenRouterModels() : []
|
|
1584
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: models })
|
|
1585
|
-
return
|
|
1586
|
-
}
|
|
1587
|
-
case "settings.getChangelog": {
|
|
1588
|
-
const releases = await fetchGitHubReleases("cuongtranba/kanna")
|
|
1589
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: releases })
|
|
1590
|
-
return
|
|
1591
|
-
}
|
|
1592
|
-
case "settings.writeLlmProvider": {
|
|
1593
|
-
const snapshot = await resolvedLlmProvider.write({
|
|
1594
|
-
provider: command.provider,
|
|
1595
|
-
apiKey: command.apiKey,
|
|
1596
|
-
model: command.model,
|
|
1597
|
-
baseUrl: command.baseUrl,
|
|
1598
|
-
})
|
|
1599
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
|
|
1600
|
-
return
|
|
1601
|
-
}
|
|
1602
|
-
case "settings.validateLlmProvider": {
|
|
1603
|
-
const result = await resolvedLlmProvider.validate({
|
|
1604
|
-
provider: command.provider,
|
|
1605
|
-
apiKey: command.apiKey,
|
|
1606
|
-
model: command.model,
|
|
1607
|
-
baseUrl: command.baseUrl,
|
|
1608
|
-
})
|
|
1609
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
1610
|
-
return
|
|
1611
|
-
}
|
|
1612
|
-
case "skills.search": {
|
|
1613
|
-
const snapshot = await searchSkills(command.query, command.limit)
|
|
1614
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
|
|
1615
|
-
return
|
|
1616
|
-
}
|
|
1617
|
-
case "skills.install": {
|
|
1618
|
-
const result = await installSkill(command.source, command.skillId)
|
|
1619
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
1620
|
-
return
|
|
1621
|
-
}
|
|
1622
|
-
case "skills.uninstall": {
|
|
1623
|
-
const result = await uninstallSkill(command.skillId)
|
|
1624
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
1625
|
-
return
|
|
1626
|
-
}
|
|
1627
|
-
case "skills.listInstalled": {
|
|
1628
|
-
const result = await listInstalledSkills()
|
|
1629
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
1630
|
-
return
|
|
1631
|
-
}
|
|
1632
|
-
case "project.open": {
|
|
1633
|
-
await ensureProjectDirectory(command.localPath)
|
|
1634
|
-
const normalizedPath = resolveLocalPath(command.localPath)
|
|
1635
|
-
const existingProjectId = store.state.projectIdsByPath.get(normalizedPath)
|
|
1636
|
-
const project = await store.openProject(command.localPath)
|
|
1637
|
-
await refreshDiscovery()
|
|
1638
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { projectId: project.id } })
|
|
1639
|
-
if (!existingProjectId) {
|
|
1640
|
-
resolvedAnalytics.track("project_opened")
|
|
1641
|
-
}
|
|
1642
|
-
break
|
|
1643
|
-
}
|
|
1644
|
-
case "project.create": {
|
|
1645
|
-
await ensureProjectDirectory(command.localPath)
|
|
1646
|
-
const normalizedPath = resolveLocalPath(command.localPath)
|
|
1647
|
-
const existingProjectId = store.state.projectIdsByPath.get(normalizedPath)
|
|
1648
|
-
const project = await store.openProject(command.localPath, command.title)
|
|
1649
|
-
await refreshDiscovery()
|
|
1650
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { projectId: project.id } })
|
|
1651
|
-
if (!existingProjectId) {
|
|
1652
|
-
resolvedAnalytics.track("project_opened")
|
|
1653
|
-
resolvedAnalytics.track("project_created")
|
|
1654
|
-
}
|
|
1655
|
-
break
|
|
1656
|
-
}
|
|
1657
|
-
case "sessions.importClaude": {
|
|
1658
|
-
const result = await importClaudeSessions({ store })
|
|
1659
|
-
if (result.newProjects > 0) {
|
|
1660
|
-
await refreshDiscovery()
|
|
1661
|
-
}
|
|
1662
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
1663
|
-
await broadcastFilteredSnapshots({ includeSidebar: true })
|
|
1664
|
-
break
|
|
1665
|
-
}
|
|
1666
|
-
case "project.remove": {
|
|
1667
|
-
const project = store.getProject(command.projectId)
|
|
1668
|
-
await store.removeProject(command.projectId)
|
|
1669
|
-
if (project) {
|
|
1670
|
-
terminals.closeByCwd(project.localPath)
|
|
1671
|
-
}
|
|
1672
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
1673
|
-
resolvedAnalytics.track("project_removed")
|
|
1674
|
-
break
|
|
1675
|
-
}
|
|
1676
|
-
case "project.setStar": {
|
|
1677
|
-
await store.setProjectStar(command.projectId, command.starred)
|
|
1678
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
1679
|
-
await broadcastFilteredSnapshots({ includeSidebar: true })
|
|
1680
|
-
return
|
|
1681
|
-
}
|
|
1682
|
-
case "sidebar.reorderProjectGroups": {
|
|
1683
|
-
await store.setSidebarProjectOrder(command.projectIds)
|
|
1684
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
1685
|
-
await broadcastFilteredSnapshots({ includeSidebar: true })
|
|
1686
|
-
return
|
|
1687
|
-
}
|
|
1688
|
-
case "project.readDiffPatch": {
|
|
1689
|
-
const project = store.getProject(command.projectId)
|
|
1690
|
-
if (!project) {
|
|
1691
|
-
throw new Error("Project not found")
|
|
1692
|
-
}
|
|
1693
|
-
const result = await resolvedDiffStore.readPatch({
|
|
1694
|
-
projectPath: project.localPath,
|
|
1695
|
-
path: command.path,
|
|
1696
|
-
})
|
|
1697
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
1698
|
-
return
|
|
1699
|
-
}
|
|
1700
|
-
case "system.openExternal": {
|
|
1701
|
-
await openExternal(command)
|
|
1702
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
1703
|
-
break
|
|
1704
|
-
}
|
|
1705
|
-
case "chat.create": {
|
|
1706
|
-
const chat = await store.createChat(command.projectId, {
|
|
1707
|
-
stackId: command.stackId,
|
|
1708
|
-
stackBindings: command.stackBindings,
|
|
1709
|
-
})
|
|
1710
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { chatId: chat.id } })
|
|
1711
|
-
resolvedAnalytics.track("chat_created")
|
|
1712
|
-
await broadcastChatAndSidebar(chat.id)
|
|
1713
|
-
return
|
|
1714
|
-
}
|
|
1715
|
-
case "chat.fork": {
|
|
1716
|
-
const result = await agent.forkChat(command.chatId)
|
|
1717
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
1718
|
-
await broadcastFilteredSnapshots({ includeSidebar: true })
|
|
1719
|
-
return
|
|
1720
|
-
}
|
|
1721
|
-
case "chat.rename": {
|
|
1722
|
-
await store.renameChat(command.chatId, command.title)
|
|
1723
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
1724
|
-
await broadcastChatAndSidebar(command.chatId)
|
|
1725
|
-
return
|
|
1726
|
-
}
|
|
1727
|
-
case "chat.archive": {
|
|
1728
|
-
await store.archiveChat(command.chatId)
|
|
1729
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
1730
|
-
await broadcastFilteredSnapshots({ includeSidebar: true })
|
|
1731
|
-
return
|
|
1732
|
-
}
|
|
1733
|
-
case "chat.unarchive": {
|
|
1734
|
-
await store.unarchiveChat(command.chatId)
|
|
1735
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
1736
|
-
await broadcastChatAndSidebar(command.chatId)
|
|
201
|
+
)
|
|
1737
202
|
return
|
|
1738
203
|
}
|
|
204
|
+
case "chat.create":
|
|
205
|
+
case "chat.fork":
|
|
206
|
+
case "chat.rename":
|
|
207
|
+
case "chat.archive":
|
|
208
|
+
case "chat.unarchive":
|
|
1739
209
|
case "chat.delete": {
|
|
1740
|
-
await
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
await agent.acceptAutoContinue(command.chatId, command.scheduleId, command.scheduledAt)
|
|
1756
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
1757
|
-
await broadcastChatAndSidebar(command.chatId)
|
|
1758
|
-
return
|
|
1759
|
-
}
|
|
1760
|
-
case "autoContinue.reschedule": {
|
|
1761
|
-
await agent.rescheduleAutoContinue(command.chatId, command.scheduleId, command.scheduledAt)
|
|
1762
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
1763
|
-
await broadcastChatAndSidebar(command.chatId)
|
|
1764
|
-
return
|
|
1765
|
-
}
|
|
1766
|
-
case "autoContinue.cancel": {
|
|
1767
|
-
await agent.cancelAutoContinue(command.chatId, command.scheduleId, "user")
|
|
1768
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
1769
|
-
await broadcastChatAndSidebar(command.chatId)
|
|
1770
|
-
return
|
|
1771
|
-
}
|
|
1772
|
-
case "tunnel.accept": {
|
|
1773
|
-
if (tunnelGateway) {
|
|
1774
|
-
await tunnelGateway.accept(command.chatId, command.tunnelId)
|
|
1775
|
-
}
|
|
1776
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
1777
|
-
await broadcastChatAndSidebar(command.chatId)
|
|
1778
|
-
return
|
|
1779
|
-
}
|
|
1780
|
-
case "tunnel.stop": {
|
|
1781
|
-
if (tunnelGateway) {
|
|
1782
|
-
await tunnelGateway.stop(command.chatId, command.tunnelId)
|
|
1783
|
-
}
|
|
1784
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
1785
|
-
await broadcastChatAndSidebar(command.chatId)
|
|
210
|
+
await handleChatCommand(
|
|
211
|
+
{
|
|
212
|
+
store,
|
|
213
|
+
agent,
|
|
214
|
+
analytics: resolvedAnalytics,
|
|
215
|
+
setDraftProtection: (chatIds) => { ws.data.protectedDraftChatIds = new Set(chatIds) },
|
|
216
|
+
logSendProfilingFn: logSendToStartingProfile,
|
|
217
|
+
send: (envelope) => send(ws, envelope),
|
|
218
|
+
broadcastChatAndSidebar: (chatId) => broadcast.broadcastChatAndSidebar(chatId),
|
|
219
|
+
broadcastSidebar: () => broadcast.broadcastFilteredSnapshots({ includeSidebar: true }),
|
|
220
|
+
broadcastAll: () => broadcast.broadcastSnapshots(),
|
|
221
|
+
},
|
|
222
|
+
command,
|
|
223
|
+
id,
|
|
224
|
+
)
|
|
1786
225
|
return
|
|
1787
226
|
}
|
|
227
|
+
case "autoContinue.accept":
|
|
228
|
+
case "autoContinue.reschedule":
|
|
229
|
+
case "autoContinue.cancel":
|
|
230
|
+
case "tunnel.accept":
|
|
231
|
+
case "tunnel.stop":
|
|
1788
232
|
case "tunnel.retry": {
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
return
|
|
1801
|
-
}
|
|
1802
|
-
case "chat.setPolicyOverride": {
|
|
1803
|
-
await store.setChatPolicyOverride(command.chatId, command.policyOverride ?? null)
|
|
1804
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
1805
|
-
await broadcastChatAndSidebar(command.chatId)
|
|
233
|
+
await handleAgentCtrlCommand(
|
|
234
|
+
{
|
|
235
|
+
agent,
|
|
236
|
+
tunnelGateway,
|
|
237
|
+
killPtyInstance,
|
|
238
|
+
send: (envelope) => send(ws, envelope),
|
|
239
|
+
broadcastChatAndSidebar: (chatId) => broadcast.broadcastChatAndSidebar(chatId),
|
|
240
|
+
},
|
|
241
|
+
command,
|
|
242
|
+
id,
|
|
243
|
+
)
|
|
1806
244
|
return
|
|
1807
245
|
}
|
|
1808
|
-
case "chat.
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
break
|
|
1812
|
-
}
|
|
246
|
+
case "chat.markRead":
|
|
247
|
+
case "chat.setPolicyOverride":
|
|
248
|
+
case "chat.setDraftProtection":
|
|
1813
249
|
case "chat.send": {
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
case "chat.
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
void broadcastSnapshots()
|
|
1846
|
-
}
|
|
1847
|
-
return
|
|
1848
|
-
}
|
|
1849
|
-
case "chat.getGitHubPublishInfo": {
|
|
1850
|
-
const { project } = resolveChatProject(command.chatId)
|
|
1851
|
-
const result = await resolvedDiffStore.getGitHubPublishInfo({
|
|
1852
|
-
projectPath: project.localPath,
|
|
1853
|
-
})
|
|
1854
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
1855
|
-
return
|
|
1856
|
-
}
|
|
1857
|
-
case "chat.checkGitHubRepoAvailability": {
|
|
1858
|
-
const result = await resolvedDiffStore.checkGitHubRepoAvailability({
|
|
1859
|
-
owner: command.owner,
|
|
1860
|
-
name: command.name,
|
|
1861
|
-
})
|
|
1862
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
1863
|
-
return
|
|
1864
|
-
}
|
|
1865
|
-
case "chat.publishToGitHub": {
|
|
1866
|
-
const { project } = resolveChatProject(command.chatId)
|
|
1867
|
-
const result = await resolvedDiffStore.publishToGitHub({
|
|
1868
|
-
projectId: project.id,
|
|
1869
|
-
projectPath: project.localPath,
|
|
1870
|
-
owner: command.owner,
|
|
1871
|
-
name: command.name,
|
|
1872
|
-
visibility: command.visibility,
|
|
1873
|
-
description: command.description,
|
|
1874
|
-
})
|
|
1875
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
1876
|
-
if (result.snapshotChanged) {
|
|
1877
|
-
void broadcastSnapshots()
|
|
1878
|
-
}
|
|
1879
|
-
return
|
|
1880
|
-
}
|
|
1881
|
-
case "chat.listBranches": {
|
|
1882
|
-
const { project } = resolveChatProject(command.chatId)
|
|
1883
|
-
const result = await resolvedDiffStore.listBranches({
|
|
1884
|
-
projectPath: project.localPath,
|
|
1885
|
-
})
|
|
1886
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
1887
|
-
return
|
|
1888
|
-
}
|
|
1889
|
-
case "chat.previewMergeBranch": {
|
|
1890
|
-
const { project } = resolveChatProject(command.chatId)
|
|
1891
|
-
const result = await resolvedDiffStore.previewMergeBranch({
|
|
1892
|
-
projectPath: project.localPath,
|
|
1893
|
-
branch: command.branch,
|
|
1894
|
-
})
|
|
1895
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
1896
|
-
return
|
|
1897
|
-
}
|
|
1898
|
-
case "chat.mergeBranch": {
|
|
1899
|
-
const { project } = resolveChatProject(command.chatId)
|
|
1900
|
-
const result = await resolvedDiffStore.mergeBranch({
|
|
1901
|
-
projectId: project.id,
|
|
1902
|
-
projectPath: project.localPath,
|
|
1903
|
-
branch: command.branch,
|
|
1904
|
-
})
|
|
1905
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
1906
|
-
if (result.snapshotChanged) {
|
|
1907
|
-
void broadcastSnapshots()
|
|
1908
|
-
}
|
|
1909
|
-
return
|
|
1910
|
-
}
|
|
1911
|
-
case "chat.checkoutBranch": {
|
|
1912
|
-
const { project } = resolveChatProject(command.chatId)
|
|
1913
|
-
const result = await resolvedDiffStore.checkoutBranch({
|
|
1914
|
-
projectId: project.id,
|
|
1915
|
-
projectPath: project.localPath,
|
|
1916
|
-
branch: command.branch,
|
|
1917
|
-
bringChanges: command.bringChanges,
|
|
1918
|
-
})
|
|
1919
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
1920
|
-
if (result.snapshotChanged) {
|
|
1921
|
-
void broadcastSnapshots()
|
|
1922
|
-
}
|
|
1923
|
-
return
|
|
1924
|
-
}
|
|
1925
|
-
case "chat.syncBranch": {
|
|
1926
|
-
const { project } = resolveChatProject(command.chatId)
|
|
1927
|
-
const result = await resolvedDiffStore.syncBranch({
|
|
1928
|
-
projectId: project.id,
|
|
1929
|
-
projectPath: project.localPath,
|
|
1930
|
-
action: command.action,
|
|
1931
|
-
})
|
|
1932
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
1933
|
-
if (result.snapshotChanged) {
|
|
1934
|
-
void broadcastSnapshots()
|
|
1935
|
-
}
|
|
1936
|
-
return
|
|
1937
|
-
}
|
|
1938
|
-
case "chat.createBranch": {
|
|
1939
|
-
const { project } = resolveChatProject(command.chatId)
|
|
1940
|
-
const result = await resolvedDiffStore.createBranch({
|
|
1941
|
-
projectId: project.id,
|
|
1942
|
-
projectPath: project.localPath,
|
|
1943
|
-
name: command.name,
|
|
1944
|
-
baseBranchName: command.baseBranchName,
|
|
1945
|
-
})
|
|
1946
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
1947
|
-
if (result.snapshotChanged) {
|
|
1948
|
-
void broadcastSnapshots()
|
|
1949
|
-
}
|
|
1950
|
-
return
|
|
1951
|
-
}
|
|
1952
|
-
case "chat.generateCommitMessage": {
|
|
1953
|
-
const { project } = resolveChatProject(command.chatId)
|
|
1954
|
-
const result = await resolvedDiffStore.generateCommitMessage({
|
|
1955
|
-
projectPath: project.localPath,
|
|
1956
|
-
paths: command.paths,
|
|
1957
|
-
})
|
|
1958
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
1959
|
-
return
|
|
1960
|
-
}
|
|
1961
|
-
case "chat.commitDiffs": {
|
|
1962
|
-
const { project } = resolveChatProject(command.chatId)
|
|
1963
|
-
const result = await resolvedDiffStore.commitFiles({
|
|
1964
|
-
projectId: project.id,
|
|
1965
|
-
projectPath: project.localPath,
|
|
1966
|
-
paths: command.paths,
|
|
1967
|
-
summary: command.summary,
|
|
1968
|
-
description: command.description,
|
|
1969
|
-
mode: command.mode,
|
|
1970
|
-
})
|
|
1971
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
1972
|
-
if (result.snapshotChanged) {
|
|
1973
|
-
void broadcastSnapshots()
|
|
1974
|
-
}
|
|
1975
|
-
return
|
|
1976
|
-
}
|
|
1977
|
-
case "chat.discardDiffFile": {
|
|
1978
|
-
const { project } = resolveChatProject(command.chatId)
|
|
1979
|
-
const result = await resolvedDiffStore.discardFile({
|
|
1980
|
-
projectId: project.id,
|
|
1981
|
-
projectPath: project.localPath,
|
|
1982
|
-
path: command.path,
|
|
1983
|
-
})
|
|
1984
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
1985
|
-
if (result.snapshotChanged) {
|
|
1986
|
-
void broadcastSnapshots()
|
|
1987
|
-
}
|
|
1988
|
-
return
|
|
1989
|
-
}
|
|
250
|
+
await handleChatCommand(
|
|
251
|
+
{
|
|
252
|
+
store,
|
|
253
|
+
agent,
|
|
254
|
+
analytics: resolvedAnalytics,
|
|
255
|
+
setDraftProtection: (chatIds) => { ws.data.protectedDraftChatIds = new Set(chatIds) },
|
|
256
|
+
logSendProfilingFn: logSendToStartingProfile,
|
|
257
|
+
send: (envelope) => send(ws, envelope),
|
|
258
|
+
broadcastChatAndSidebar: (chatId) => broadcast.broadcastChatAndSidebar(chatId),
|
|
259
|
+
broadcastSidebar: () => broadcast.broadcastFilteredSnapshots({ includeSidebar: true }),
|
|
260
|
+
broadcastAll: () => broadcast.broadcastSnapshots(),
|
|
261
|
+
},
|
|
262
|
+
command,
|
|
263
|
+
id,
|
|
264
|
+
)
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
case "chat.refreshDiffs":
|
|
268
|
+
case "chat.initGit":
|
|
269
|
+
case "chat.getGitHubPublishInfo":
|
|
270
|
+
case "chat.checkGitHubRepoAvailability":
|
|
271
|
+
case "chat.publishToGitHub":
|
|
272
|
+
case "chat.listBranches":
|
|
273
|
+
case "chat.previewMergeBranch":
|
|
274
|
+
case "chat.mergeBranch":
|
|
275
|
+
case "chat.checkoutBranch":
|
|
276
|
+
case "chat.syncBranch":
|
|
277
|
+
case "chat.createBranch":
|
|
278
|
+
case "chat.generateCommitMessage":
|
|
279
|
+
case "chat.commitDiffs":
|
|
280
|
+
case "chat.discardDiffFile":
|
|
1990
281
|
case "chat.ignoreDiffFile": {
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
return
|
|
2002
|
-
}
|
|
2003
|
-
case "chat.cancel": {
|
|
2004
|
-
await agent.cancel(command.chatId)
|
|
2005
|
-
// Resolve any open ask-style tool-callback prompts for this chat
|
|
2006
|
-
// so the model's tool_use does not hang on a stranded pending. The
|
|
2007
|
-
// session-close path no longer fires this cascade because it also
|
|
2008
|
-
// ran on transparent respawns (rotation / idle sweep) — see
|
|
2009
|
-
// makeClaudeSessionHandle.close() in agent.ts.
|
|
2010
|
-
if (agent.toolCallbackService) {
|
|
2011
|
-
await agent.toolCallbackService.cancelAllForChat(command.chatId, "chat_cancelled")
|
|
2012
|
-
}
|
|
2013
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
2014
|
-
return
|
|
2015
|
-
}
|
|
2016
|
-
case "chat.stopDraining": {
|
|
2017
|
-
await agent.stopDraining(command.chatId)
|
|
2018
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
2019
|
-
return
|
|
2020
|
-
}
|
|
2021
|
-
case "chat.loadHistory": {
|
|
2022
|
-
const chat = store.getChat(command.chatId)
|
|
2023
|
-
if (!chat) throw new Error("Chat not found")
|
|
2024
|
-
const page = store.getMessagesPageBefore(command.chatId, command.beforeCursor, command.limit)
|
|
2025
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: page })
|
|
2026
|
-
return
|
|
2027
|
-
}
|
|
2028
|
-
case "chat.respondTool": {
|
|
2029
|
-
await agent.respondTool(command)
|
|
2030
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
2031
|
-
return
|
|
2032
|
-
}
|
|
2033
|
-
case "chat.toolRequestAnswer": {
|
|
2034
|
-
const toolCallbackSvc = agent.toolCallbackService
|
|
2035
|
-
if (!toolCallbackSvc) throw new Error("tool callback service unavailable")
|
|
2036
|
-
const validKinds = new Set(["allow", "deny", "answer"])
|
|
2037
|
-
if (!isRecord(command.decision) || !validKinds.has(typeof command.decision.kind === "string" ? command.decision.kind : "")) {
|
|
2038
|
-
throw new Error("Invalid tool request decision kind")
|
|
2039
|
-
}
|
|
2040
|
-
const existing = store.getToolRequest(command.toolRequestId)
|
|
2041
|
-
if (!existing || existing.chatId !== command.chatId) {
|
|
2042
|
-
throw new Error("Tool request does not belong to this chat")
|
|
2043
|
-
}
|
|
2044
|
-
await toolCallbackSvc.answer(command.toolRequestId, command.decision)
|
|
2045
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
2046
|
-
await broadcastChatAndSidebar(command.chatId)
|
|
2047
|
-
return
|
|
2048
|
-
}
|
|
2049
|
-
case "chat.respondSubagentTool": {
|
|
2050
|
-
await agent.respondSubagentTool(command)
|
|
2051
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
282
|
+
await handleDiffCommand(
|
|
283
|
+
{
|
|
284
|
+
resolvedDiffStore,
|
|
285
|
+
resolveChatProject,
|
|
286
|
+
send: (envelope) => send(ws, envelope),
|
|
287
|
+
broadcastSnapshots: () => broadcast.broadcastSnapshots(),
|
|
288
|
+
},
|
|
289
|
+
command,
|
|
290
|
+
id,
|
|
291
|
+
)
|
|
2052
292
|
return
|
|
2053
293
|
}
|
|
294
|
+
case "chat.cancel":
|
|
295
|
+
case "chat.stopDraining":
|
|
296
|
+
case "chat.loadHistory":
|
|
297
|
+
case "chat.respondTool":
|
|
298
|
+
case "chat.toolRequestAnswer":
|
|
299
|
+
case "chat.respondSubagentTool":
|
|
2054
300
|
case "chat.cancelSubagentRun": {
|
|
2055
|
-
await
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
}
|
|
2071
|
-
case "message.dequeue": {
|
|
2072
|
-
await agent.dequeue(command)
|
|
2073
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
2074
|
-
await broadcastChatAndSidebar(command.chatId)
|
|
2075
|
-
return
|
|
2076
|
-
}
|
|
2077
|
-
case "terminal.create": {
|
|
2078
|
-
const project = store.getProject(command.projectId)
|
|
2079
|
-
if (!project) {
|
|
2080
|
-
throw new Error("Project not found")
|
|
2081
|
-
}
|
|
2082
|
-
const snapshot = terminals.createTerminal({
|
|
2083
|
-
projectPath: project.localPath,
|
|
2084
|
-
terminalId: command.terminalId,
|
|
2085
|
-
cols: command.cols,
|
|
2086
|
-
rows: command.rows,
|
|
2087
|
-
scrollback: command.scrollback,
|
|
2088
|
-
})
|
|
2089
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
|
|
2090
|
-
return
|
|
2091
|
-
}
|
|
2092
|
-
case "terminal.input": {
|
|
2093
|
-
terminals.write(command.terminalId, command.data)
|
|
2094
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
2095
|
-
return
|
|
2096
|
-
}
|
|
2097
|
-
case "terminal.resize": {
|
|
2098
|
-
terminals.resize(command.terminalId, command.cols, command.rows)
|
|
2099
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
301
|
+
await handleChatCommand(
|
|
302
|
+
{
|
|
303
|
+
store,
|
|
304
|
+
agent,
|
|
305
|
+
analytics: resolvedAnalytics,
|
|
306
|
+
setDraftProtection: (chatIds) => { ws.data.protectedDraftChatIds = new Set(chatIds) },
|
|
307
|
+
logSendProfilingFn: logSendToStartingProfile,
|
|
308
|
+
send: (envelope) => send(ws, envelope),
|
|
309
|
+
broadcastChatAndSidebar: (chatId) => broadcast.broadcastChatAndSidebar(chatId),
|
|
310
|
+
broadcastSidebar: () => broadcast.broadcastFilteredSnapshots({ includeSidebar: true }),
|
|
311
|
+
broadcastAll: () => broadcast.broadcastSnapshots(),
|
|
312
|
+
},
|
|
313
|
+
command,
|
|
314
|
+
id,
|
|
315
|
+
)
|
|
2100
316
|
return
|
|
2101
317
|
}
|
|
318
|
+
case "message.enqueue":
|
|
319
|
+
case "message.steer":
|
|
320
|
+
case "message.dequeue":
|
|
321
|
+
case "terminal.create":
|
|
322
|
+
case "terminal.input":
|
|
323
|
+
case "terminal.resize":
|
|
2102
324
|
case "terminal.close": {
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
label: command.label,
|
|
2121
|
-
userAgent: command.userAgent,
|
|
2122
|
-
})
|
|
2123
|
-
ws.data.pushDeviceId = result.id
|
|
2124
|
-
await broadcastFilteredSnapshots({ includePushConfig: true })
|
|
2125
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
2126
|
-
return
|
|
2127
|
-
}
|
|
2128
|
-
case "push.unsubscribe": {
|
|
2129
|
-
await pushManager.removeSubscription(command.pushDeviceId, "user_revoked")
|
|
2130
|
-
if (ws.data.pushDeviceId === command.pushDeviceId) {
|
|
2131
|
-
ws.data.pushDeviceId = null
|
|
2132
|
-
}
|
|
2133
|
-
await broadcastFilteredSnapshots({ includePushConfig: true })
|
|
2134
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
2135
|
-
return
|
|
2136
|
-
}
|
|
2137
|
-
case "push.test": {
|
|
2138
|
-
if (ws.data.pushDeviceId) {
|
|
2139
|
-
await pushManager.sendTest(ws.data.pushDeviceId)
|
|
2140
|
-
}
|
|
2141
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
2142
|
-
return
|
|
2143
|
-
}
|
|
2144
|
-
case "push.setProjectMute": {
|
|
2145
|
-
await pushManager.setProjectMute(command.localPath, command.muted)
|
|
2146
|
-
await broadcastFilteredSnapshots({ includePushConfig: true })
|
|
2147
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
325
|
+
await handleMiscCommand(
|
|
326
|
+
{
|
|
327
|
+
store,
|
|
328
|
+
terminals,
|
|
329
|
+
agent,
|
|
330
|
+
sessionShare,
|
|
331
|
+
analytics: resolvedAnalytics,
|
|
332
|
+
listWorktrees,
|
|
333
|
+
getOriginHost: () => ws.data.originHost ?? "",
|
|
334
|
+
send: (envelope) => send(ws, envelope),
|
|
335
|
+
broadcastSidebar: () => broadcast.broadcastFilteredSnapshots({ includeSidebar: true }),
|
|
336
|
+
broadcastChatAndSidebar: (chatId) => broadcast.broadcastChatAndSidebar(chatId),
|
|
337
|
+
pushTerminalSnapshot: (terminalId) => broadcast.pushTerminalSnapshot(terminalId),
|
|
338
|
+
},
|
|
339
|
+
command,
|
|
340
|
+
id,
|
|
341
|
+
)
|
|
2148
342
|
return
|
|
2149
343
|
}
|
|
344
|
+
case "push.identifyDevice":
|
|
345
|
+
case "push.subscribe":
|
|
346
|
+
case "push.unsubscribe":
|
|
347
|
+
case "push.test":
|
|
348
|
+
case "push.setProjectMute":
|
|
2150
349
|
case "push.setFocusedChat": {
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
send(ws, {
|
|
2163
|
-
v: PROTOCOL_VERSION,
|
|
2164
|
-
type: "ack",
|
|
2165
|
-
id,
|
|
2166
|
-
result: { ok: false, error: err instanceof Error ? err.message : String(err) },
|
|
2167
|
-
})
|
|
2168
|
-
}
|
|
350
|
+
await handlePushCommand(
|
|
351
|
+
{
|
|
352
|
+
pushManager,
|
|
353
|
+
getPushDeviceId: () => ws.data.pushDeviceId,
|
|
354
|
+
setPushDeviceId: (did) => { ws.data.pushDeviceId = did },
|
|
355
|
+
send: (envelope) => send(ws, envelope),
|
|
356
|
+
broadcastPushConfig: () => broadcast.broadcastFilteredSnapshots({ includePushConfig: true }),
|
|
357
|
+
},
|
|
358
|
+
command,
|
|
359
|
+
id,
|
|
360
|
+
)
|
|
2169
361
|
return
|
|
2170
362
|
}
|
|
363
|
+
case "pty.cancel":
|
|
2171
364
|
case "pty.kill": {
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
return
|
|
2184
|
-
}
|
|
2185
|
-
case "stack.create": {
|
|
2186
|
-
const stack = await store.createStack(command.title, command.projectIds)
|
|
2187
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { stackId: stack.id } })
|
|
2188
|
-
resolvedAnalytics.track("stack_created")
|
|
2189
|
-
await broadcastFilteredSnapshots({ includeSidebar: true })
|
|
2190
|
-
return
|
|
2191
|
-
}
|
|
2192
|
-
case "stack.rename": {
|
|
2193
|
-
await store.renameStack(command.stackId, command.title)
|
|
2194
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
2195
|
-
await broadcastFilteredSnapshots({ includeSidebar: true })
|
|
2196
|
-
return
|
|
2197
|
-
}
|
|
2198
|
-
case "stack.remove": {
|
|
2199
|
-
await store.removeStack(command.stackId)
|
|
2200
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
2201
|
-
await broadcastFilteredSnapshots({ includeSidebar: true })
|
|
2202
|
-
return
|
|
2203
|
-
}
|
|
2204
|
-
case "stack.addProject": {
|
|
2205
|
-
await store.addProjectToStack(command.stackId, command.projectId)
|
|
2206
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
2207
|
-
await broadcastFilteredSnapshots({ includeSidebar: true })
|
|
2208
|
-
return
|
|
2209
|
-
}
|
|
2210
|
-
case "stack.removeProject": {
|
|
2211
|
-
await store.removeProjectFromStack(command.stackId, command.projectId)
|
|
2212
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
2213
|
-
await broadcastFilteredSnapshots({ includeSidebar: true })
|
|
2214
|
-
return
|
|
2215
|
-
}
|
|
2216
|
-
case "stack.listWorktrees": {
|
|
2217
|
-
const project = store.getProject(command.projectId)
|
|
2218
|
-
if (!project) {
|
|
2219
|
-
throw new Error("Project not found")
|
|
2220
|
-
}
|
|
2221
|
-
const worktrees = await listWorktrees(project.localPath)
|
|
2222
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { worktrees } })
|
|
2223
|
-
return
|
|
2224
|
-
}
|
|
2225
|
-
case "share.mint": {
|
|
2226
|
-
if (!sessionShare) {
|
|
2227
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { ok: false, error: { kind: "snapshot_write_failed", message: "session-share service unavailable" } } })
|
|
2228
|
-
return
|
|
2229
|
-
}
|
|
2230
|
-
const r = await sessionShare.mintToken(command.payload, ws.data.originHost ?? "")
|
|
2231
|
-
const result: ShareCommandResult = r.ok
|
|
2232
|
-
? { ok: true, kind: "mint", data: r.data }
|
|
2233
|
-
: { ok: false, error: r.error }
|
|
2234
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
2235
|
-
return
|
|
2236
|
-
}
|
|
2237
|
-
case "share.revoke": {
|
|
2238
|
-
if (!sessionShare) {
|
|
2239
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { ok: false, error: { kind: "not_found" } } })
|
|
2240
|
-
return
|
|
2241
|
-
}
|
|
2242
|
-
const r = await sessionShare.revokeToken(command.payload)
|
|
2243
|
-
const result: ShareCommandResult = r.ok
|
|
2244
|
-
? { ok: true, kind: "revoke", data: r.data }
|
|
2245
|
-
: { ok: false, error: r.error }
|
|
2246
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
365
|
+
await handleAgentCtrlCommand(
|
|
366
|
+
{
|
|
367
|
+
agent,
|
|
368
|
+
tunnelGateway,
|
|
369
|
+
killPtyInstance,
|
|
370
|
+
send: (envelope) => send(ws, envelope),
|
|
371
|
+
broadcastChatAndSidebar: (chatId) => broadcast.broadcastChatAndSidebar(chatId),
|
|
372
|
+
},
|
|
373
|
+
command,
|
|
374
|
+
id,
|
|
375
|
+
)
|
|
2247
376
|
return
|
|
2248
377
|
}
|
|
378
|
+
case "stack.create":
|
|
379
|
+
case "stack.rename":
|
|
380
|
+
case "stack.remove":
|
|
381
|
+
case "stack.addProject":
|
|
382
|
+
case "stack.removeProject":
|
|
383
|
+
case "stack.listWorktrees":
|
|
384
|
+
case "share.mint":
|
|
385
|
+
case "share.revoke":
|
|
2249
386
|
case "share.list": {
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
}
|
|
2268
|
-
case "subagents.getRun": {
|
|
2269
|
-
const entries = subagentTranscriptRegistry?.getAgentTranscript(command.chatId, command.agentId) ?? []
|
|
2270
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: entries })
|
|
2271
|
-
return
|
|
2272
|
-
}
|
|
2273
|
-
case "orch.run": {
|
|
2274
|
-
const result = await agent.runOrchestration(command.chatId, command.input)
|
|
2275
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
2276
|
-
return
|
|
2277
|
-
}
|
|
2278
|
-
case "orch.cancelRun": {
|
|
2279
|
-
await agent.cancelOrchRun(command.runId)
|
|
2280
|
-
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result: { ok: true } })
|
|
387
|
+
await handleMiscCommand(
|
|
388
|
+
{
|
|
389
|
+
store,
|
|
390
|
+
terminals,
|
|
391
|
+
agent,
|
|
392
|
+
sessionShare,
|
|
393
|
+
analytics: resolvedAnalytics,
|
|
394
|
+
listWorktrees,
|
|
395
|
+
getOriginHost: () => ws.data.originHost ?? "",
|
|
396
|
+
send: (envelope) => send(ws, envelope),
|
|
397
|
+
broadcastSidebar: () => broadcast.broadcastFilteredSnapshots({ includeSidebar: true }),
|
|
398
|
+
broadcastChatAndSidebar: (chatId) => broadcast.broadcastChatAndSidebar(chatId),
|
|
399
|
+
pushTerminalSnapshot: (terminalId) => broadcast.pushTerminalSnapshot(terminalId),
|
|
400
|
+
},
|
|
401
|
+
command,
|
|
402
|
+
id,
|
|
403
|
+
)
|
|
2281
404
|
return
|
|
2282
405
|
}
|
|
406
|
+
case "workflows.getRun":
|
|
407
|
+
case "workflows.getAgentTranscript":
|
|
408
|
+
case "subagents.getRun":
|
|
409
|
+
case "orch.run":
|
|
410
|
+
case "orch.cancelRun":
|
|
2283
411
|
case "orch.getRun": {
|
|
2284
|
-
|
|
412
|
+
await handleOrchCommand(
|
|
413
|
+
{
|
|
414
|
+
agent,
|
|
415
|
+
workflowRegistry,
|
|
416
|
+
subagentTranscriptRegistry,
|
|
417
|
+
send: (envelope) => send(ws, envelope),
|
|
418
|
+
},
|
|
419
|
+
command,
|
|
420
|
+
id,
|
|
421
|
+
)
|
|
422
|
+
return
|
|
423
|
+
}
|
|
424
|
+
case "system.ping":
|
|
425
|
+
case "system.openExternal":
|
|
426
|
+
case "update.check":
|
|
427
|
+
case "update.install":
|
|
428
|
+
case "update.reload":
|
|
429
|
+
case "project.open":
|
|
430
|
+
case "project.create":
|
|
431
|
+
case "project.remove":
|
|
432
|
+
case "project.setStar":
|
|
433
|
+
case "project.readDiffPatch":
|
|
434
|
+
case "sessions.importClaude":
|
|
435
|
+
case "sidebar.reorderProjectGroups": {
|
|
436
|
+
await handleProjectCommand(
|
|
437
|
+
{
|
|
438
|
+
store,
|
|
439
|
+
updateManager,
|
|
440
|
+
diffStore: resolvedDiffStore,
|
|
441
|
+
analytics: resolvedAnalytics,
|
|
442
|
+
refreshDiscovery,
|
|
443
|
+
ensureProjectDirectory,
|
|
444
|
+
resolveLocalPath,
|
|
445
|
+
importClaudeSessionsFn: () => importClaudeSessions({ store }),
|
|
446
|
+
openExternalFn: openExternal,
|
|
447
|
+
terminals,
|
|
448
|
+
send: (envelope) => send(ws, envelope),
|
|
449
|
+
broadcastSidebar: () => broadcast.broadcastFilteredSnapshots({ includeSidebar: true }),
|
|
450
|
+
},
|
|
451
|
+
command,
|
|
452
|
+
id,
|
|
453
|
+
)
|
|
2285
454
|
return
|
|
2286
455
|
}
|
|
2287
456
|
}
|
|
2288
457
|
|
|
2289
|
-
await broadcastSnapshots()
|
|
458
|
+
await broadcast.broadcastSnapshots()
|
|
2290
459
|
} catch (error) {
|
|
2291
460
|
const messageText = error instanceof Error ? error.message : String(error)
|
|
2292
461
|
const benign = isBenignStaleStateMessage(messageText)
|
|
@@ -2302,19 +471,19 @@ export function createWsRouter({
|
|
|
2302
471
|
|
|
2303
472
|
return {
|
|
2304
473
|
handleOpen(ws: ServerWebSocket<ClientState>) {
|
|
2305
|
-
|
|
474
|
+
broadcast.addSocket(ws)
|
|
2306
475
|
},
|
|
2307
476
|
handleClose(ws: ServerWebSocket<ClientState>) {
|
|
2308
477
|
if (ws.data.pushDeviceId) {
|
|
2309
478
|
pushManager.clearFocus(ws.data.pushDeviceId)
|
|
2310
479
|
}
|
|
2311
|
-
|
|
480
|
+
broadcast.removeSocket(ws)
|
|
2312
481
|
},
|
|
2313
|
-
broadcastSnapshots,
|
|
2314
|
-
broadcastChatStateImmediately,
|
|
2315
|
-
scheduleBroadcast,
|
|
2316
|
-
scheduleChatStateBroadcast,
|
|
2317
|
-
pruneStaleEmptyChats: () => maybePruneStaleEmptyChats(),
|
|
482
|
+
broadcastSnapshots: () => broadcast.broadcastSnapshots(),
|
|
483
|
+
broadcastChatStateImmediately: (chatId: string) => broadcast.broadcastChatStateImmediately(chatId),
|
|
484
|
+
scheduleBroadcast: () => broadcast.scheduleBroadcast(),
|
|
485
|
+
scheduleChatStateBroadcast: (chatId: string) => broadcast.scheduleChatStateBroadcast(chatId),
|
|
486
|
+
pruneStaleEmptyChats: () => broadcast.maybePruneStaleEmptyChats(),
|
|
2318
487
|
async handleMessage(ws: ServerWebSocket<ClientState>, raw: string | Buffer | ArrayBuffer | Uint8Array) {
|
|
2319
488
|
let parsed: AnyValue
|
|
2320
489
|
try {
|
|
@@ -2339,12 +508,12 @@ export function createWsRouter({
|
|
|
2339
508
|
if (parsed.topic.type === "local-projects") {
|
|
2340
509
|
void refreshDiscovery().then(() => {
|
|
2341
510
|
if (ws.data.subscriptions.has(parsed.id)) {
|
|
2342
|
-
void pushSnapshots(ws, { skipPrune: true })
|
|
511
|
+
void broadcast.pushSnapshots(ws, { skipPrune: true })
|
|
2343
512
|
}
|
|
2344
513
|
})
|
|
2345
514
|
return
|
|
2346
515
|
}
|
|
2347
|
-
await pushSnapshots(ws, { skipPrune: true })
|
|
516
|
+
await broadcast.pushSnapshots(ws, { skipPrune: true })
|
|
2348
517
|
return
|
|
2349
518
|
}
|
|
2350
519
|
|
|
@@ -2359,91 +528,7 @@ export function createWsRouter({
|
|
|
2359
528
|
await handleCommand(ws, parsed)
|
|
2360
529
|
},
|
|
2361
530
|
dispose() {
|
|
2362
|
-
|
|
2363
|
-
clearTimeout(pendingBroadcastTimer)
|
|
2364
|
-
}
|
|
2365
|
-
agent.setBackgroundErrorReporter?.(null)
|
|
2366
|
-
disposeTerminalEvents()
|
|
2367
|
-
disposeKeybindingEvents()
|
|
2368
|
-
disposeAppSettingsEvents()
|
|
2369
|
-
disposeUpdateEvents()
|
|
2370
|
-
disposePtyInstances()
|
|
2371
|
-
disposeWorkflows()
|
|
2372
|
-
disposeOrchRuns()
|
|
531
|
+
broadcast.dispose()
|
|
2373
532
|
},
|
|
2374
533
|
}
|
|
2375
534
|
}
|
|
2376
|
-
|
|
2377
|
-
async function testOAuthToken(token: string): Promise<{ ok: boolean; error: string | null }> {
|
|
2378
|
-
const trimmed = typeof token === "string" ? token.trim() : ""
|
|
2379
|
-
if (!trimmed) return { ok: false, error: "Token is empty" }
|
|
2380
|
-
try {
|
|
2381
|
-
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
2382
|
-
method: "POST",
|
|
2383
|
-
headers: {
|
|
2384
|
-
"anthropic-version": "2023-06-01",
|
|
2385
|
-
"content-type": "application/json",
|
|
2386
|
-
"authorization": `Bearer ${trimmed}`,
|
|
2387
|
-
},
|
|
2388
|
-
body: JSON.stringify({
|
|
2389
|
-
model: "claude-haiku-4-5-20251001",
|
|
2390
|
-
max_tokens: 1,
|
|
2391
|
-
messages: [{ role: "user", content: "ok" }],
|
|
2392
|
-
}),
|
|
2393
|
-
signal: AbortSignal.timeout(10_000),
|
|
2394
|
-
})
|
|
2395
|
-
if (res.status === 401 || res.status === 403) return { ok: false, error: "Unauthorized" }
|
|
2396
|
-
if (res.status === 429) return { ok: true, error: "Token valid but currently rate-limited" }
|
|
2397
|
-
if (!res.ok) return { ok: false, error: `HTTP ${res.status}` }
|
|
2398
|
-
return { ok: true, error: null }
|
|
2399
|
-
} catch (err) {
|
|
2400
|
-
if (err instanceof Error && err.name === "TimeoutError") {
|
|
2401
|
-
return { ok: false, error: "Request timed out after 10s" }
|
|
2402
|
-
}
|
|
2403
|
-
return { ok: false, error: err instanceof Error ? err.message : String(err) }
|
|
2404
|
-
}
|
|
2405
|
-
}
|
|
2406
|
-
|
|
2407
|
-
/**
|
|
2408
|
-
* For an OAuth-authenticated network server, resolve a fresh access token to
|
|
2409
|
-
* inject as a Bearer when probing it — the manual "Test" / auto-test path is
|
|
2410
|
-
* otherwise tokenless and a healthy OAuth server 401s. Returns undefined for
|
|
2411
|
-
* stdio, static-header, or not-yet-authenticated servers (the probe runs with
|
|
2412
|
-
* stored headers only). A refresh failure also yields undefined, so the probe
|
|
2413
|
-
* surfaces the unauthorized error that correctly signals re-auth is needed.
|
|
2414
|
-
*/
|
|
2415
|
-
export async function resolveMcpTestBearer(
|
|
2416
|
-
entry: McpServerConfig,
|
|
2417
|
-
appSettings: { writePatch(p: AppSettingsPatch): Promise<unknown> },
|
|
2418
|
-
): Promise<string | undefined> {
|
|
2419
|
-
if (entry.transport === "stdio" || entry.oauth?.status !== "authenticated") return undefined
|
|
2420
|
-
try {
|
|
2421
|
-
return await ensureFreshMcpToken(entry, {
|
|
2422
|
-
persist: (oauth) =>
|
|
2423
|
-
void appSettings.writePatch({ customMcpServers: { setOAuthState: { id: entry.id, oauth } } }),
|
|
2424
|
-
})
|
|
2425
|
-
} catch {
|
|
2426
|
-
return undefined
|
|
2427
|
-
}
|
|
2428
|
-
}
|
|
2429
|
-
|
|
2430
|
-
async function runMcpAutoTest(
|
|
2431
|
-
id: string,
|
|
2432
|
-
appSettings: { getSnapshot(): AppSettingsSnapshot; writePatch(p: AppSettingsPatch): Promise<unknown> },
|
|
2433
|
-
): Promise<void> {
|
|
2434
|
-
try {
|
|
2435
|
-
const entry = appSettings.getSnapshot().customMcpServers.find((s) => s.id === id)
|
|
2436
|
-
if (!entry) return
|
|
2437
|
-
await appSettings.writePatch({
|
|
2438
|
-
customMcpServers: {
|
|
2439
|
-
setTestResult: { id, result: { status: "pending", startedAt: new Date().toISOString() } },
|
|
2440
|
-
},
|
|
2441
|
-
})
|
|
2442
|
-
const bearer = await resolveMcpTestBearer(entry, appSettings)
|
|
2443
|
-
const result = await validateMcpServer(entry, bearer ? { bearer } : {})
|
|
2444
|
-
await appSettings.writePatch({ customMcpServers: { setTestResult: { id, result } } })
|
|
2445
|
-
} catch (err) {
|
|
2446
|
-
// Auto-test must never throw; log + swallow.
|
|
2447
|
-
log.warn("[kanna/ws-router] runMcpAutoTest failed", String(err))
|
|
2448
|
-
}
|
|
2449
|
-
}
|