@cuongtran001/kanna 1.3.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-CAysfmX5.js → arc-DXtN3EWJ.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-BKqRYsm4.js → architectureDiagram-3BPJPVTR-CIGqW1z-.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-Ce7Eorec.js → blockDiagram-GPEHLZMM-BafNS_H6.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-CJ77zauH.js → c4Diagram-AAUBKEIU-YdAgRSis.js} +1 -1
- package/dist/client/assets/channel-BEYTrUAm.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-Rk_3zLxJ.js → chunk-2J33WTMH-eiP5y5G-.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-DPIz6Nvf.js → chunk-4BX2VUAB-CvLGzPMF.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-D-TkbGh2.js → chunk-55IACEB6-Y-S9Fb8A.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-B4NOSIZV.js → chunk-727SXJPM-6sJH1RI9.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-Cm_TGLYm.js → chunk-AQP2D5EJ-hV1r_IXO.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-Bu45BUVh.js → chunk-FMBD7UC4-CC3L28S5.js} +1 -1
- package/dist/client/assets/{chunk-ND2GUHAM-C1Bw8_gg.js → chunk-ND2GUHAM-BZ3czhdH.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-DueG15o0.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-YgpMurXf.js → cose-bilkent-S5V4N54A-CfxdDghE.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG-Dop7Oq67.js → dagre-BM42HDAG-B8CddsbB.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-unMfBEJH.js → diagram-2AECGRRQ-F0yS0fVB.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-D9EJ9qMI.js → diagram-5GNKFQAL-cW0i0qlh.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-C5IykOIR.js → diagram-KO2AKTUF-BzAKMeJT.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-DJAuPngz.js → diagram-LMA3HP47-qdC4XqIl.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-B3wHFtS-.js → diagram-OG6HWLK6-CGcpi6Dh.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-Dm7kt9o5.js → erDiagram-TEJ5UH35-CxNTDicM.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-s9Lds3ny.js → flowDiagram-I6XJVG4X-qkGa5TqY.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-CRQNYdAf.js → ganttDiagram-6RSMTGT7-D9mVPoQi.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DhYjK4Jb.js → gitGraphDiagram-PVQCEYII-DA0grAXe.js} +1 -1
- package/dist/client/assets/{index-CbSY9HfS.js → index-BqUY1Bm4.js} +1 -1
- package/dist/client/assets/{index-C0U2kwuB.js → index-TU1k6ov6.js} +5 -5
- package/dist/client/assets/{infoDiagram-5YYISTIA-CRKJ0PSH.js → infoDiagram-5YYISTIA-CGljtLs3.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CEAkB0IY.js → ishikawaDiagram-YF4QCWOH-CtC0xrmG.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-CyOK5_DG.js → journeyDiagram-JHISSGLW-URLnVZeN.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-BfKjiJSo.js → kanban-definition-UN3LZRKU-C2Q6aqQh.js} +1 -1
- package/dist/client/assets/{linear-DLR6miy3.js → linear-ByKHtsIx.js} +1 -1
- package/dist/client/assets/{mermaid.core-D7AdC_lE.js → mermaid.core-Dl_f2KLc.js} +4 -4
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-zYyJtDSa.js → mindmap-definition-RKZ34NQL-CaJ9IDLI.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-BgP8PFlo.js → pieDiagram-4H26LBE5-mmYaf2Oa.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Di8h7J96.js → quadrantDiagram-W4KKPZXB-B_PO6OCC.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-DuTzFl3R.js → requirementDiagram-4Y6WPE33-M4huwJt1.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DBRoMm_T.js → sankeyDiagram-5OEKKPKP-B0hb1sWh.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-Bmb-1HUN.js → sequenceDiagram-3UESZ5HK-BAqSptnA.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-fnv5ayn-.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-ByWRdpQX.js → timeline-definition-PNZ67QCA-MgAjHz8Q.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN--2WGpVXW.js → vennDiagram-CIIHVFJN-DmKoO51W.js} +1 -1
- package/dist/client/assets/{wardley-L42UT6IY-24zlnzxK.js → wardley-L42UT6IY-CWcyHnYs.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Cllvqb3K.js → wardleyDiagram-YWT4CUSO-BGIGmFP9.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-ZcdC6nPE.js → xychartDiagram-2RQKCTM6-D20YIvEl.js} +1 -1
- package/dist/client/index.html +1 -1
- 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.test.ts +22 -6
- package/src/server/agent.ts +35 -4987
- 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 -2285
- package/src/server/harness-types.ts +15 -1
- package/src/server/oauth-errors.ts +19 -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/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 +25 -1711
- package/dist/client/assets/channel-KQhRwDzJ.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-ChxT1GNq.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-ChxT1GNq.js +0 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-D4X-ZnLV.js +0 -1
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ws-router-misc.test.ts
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for handleMiscCommand (terminal, message, stack, share groups).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, mock, test } from "bun:test"
|
|
8
|
+
import type {
|
|
9
|
+
MiscAgentDep,
|
|
10
|
+
MiscAnalyticsDep,
|
|
11
|
+
MiscCommandDeps,
|
|
12
|
+
MiscSessionShareDep,
|
|
13
|
+
MiscStoreDep,
|
|
14
|
+
MiscTerminalsDep,
|
|
15
|
+
} from "./ws-router-misc"
|
|
16
|
+
import { handleMiscCommand } from "./ws-router-misc"
|
|
17
|
+
import type { ClientCommand, ServerEnvelope } from "../shared/protocol"
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Helpers
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
function makeStore(overrides: Partial<MiscStoreDep> = {}): MiscStoreDep {
|
|
24
|
+
return {
|
|
25
|
+
getProject: mock(() => null),
|
|
26
|
+
createStack: mock(async () => ({ id: "stack-1" })),
|
|
27
|
+
renameStack: mock(async () => {}),
|
|
28
|
+
removeStack: mock(async () => {}),
|
|
29
|
+
addProjectToStack: mock(async () => {}),
|
|
30
|
+
removeProjectFromStack: mock(async () => {}),
|
|
31
|
+
...overrides,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeTerminals(overrides: Partial<MiscTerminalsDep> = {}): MiscTerminalsDep {
|
|
36
|
+
return {
|
|
37
|
+
createTerminal: mock(() => ({
|
|
38
|
+
terminalId: "term-1",
|
|
39
|
+
title: "bash",
|
|
40
|
+
cwd: "/tmp",
|
|
41
|
+
shell: "/bin/bash",
|
|
42
|
+
cols: 80,
|
|
43
|
+
rows: 24,
|
|
44
|
+
scrollback: 1000,
|
|
45
|
+
serializedState: "",
|
|
46
|
+
status: "running" as const,
|
|
47
|
+
exitCode: null,
|
|
48
|
+
})),
|
|
49
|
+
write: mock(() => {}),
|
|
50
|
+
resize: mock(() => {}),
|
|
51
|
+
close: mock(() => {}),
|
|
52
|
+
...overrides,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function makeAgent(overrides: Partial<MiscAgentDep> = {}): MiscAgentDep {
|
|
57
|
+
return {
|
|
58
|
+
enqueue: mock(async () => ({ queuedMessageId: "msg-1" })),
|
|
59
|
+
steer: mock(async () => {}),
|
|
60
|
+
dequeue: mock(async () => {}),
|
|
61
|
+
...overrides,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function makeSessionShare(overrides: Partial<MiscSessionShareDep> = {}): MiscSessionShareDep {
|
|
66
|
+
return {
|
|
67
|
+
mintToken: mock(async () => ({
|
|
68
|
+
ok: true as const,
|
|
69
|
+
data: {
|
|
70
|
+
summary: {
|
|
71
|
+
tokenId: "tok-1",
|
|
72
|
+
chatId: "chat-1",
|
|
73
|
+
createdAt: 0,
|
|
74
|
+
expiresAt: 0,
|
|
75
|
+
url: "https://example.com/share/tok-1",
|
|
76
|
+
revoked: false,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
})),
|
|
80
|
+
revokeToken: mock(async () => ({ ok: true as const, data: { tokenId: "tok-1" } })),
|
|
81
|
+
listSharesForChat: mock(() => []),
|
|
82
|
+
...overrides,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface TestDeps extends MiscCommandDeps {
|
|
87
|
+
sent: unknown[]
|
|
88
|
+
sideBroadcasts: number
|
|
89
|
+
chatBroadcasts: string[]
|
|
90
|
+
pushSnapshots: string[]
|
|
91
|
+
worktreeCallPaths: string[]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function makeDeps(opts: {
|
|
95
|
+
storeOverrides?: Partial<MiscStoreDep>
|
|
96
|
+
terminalsOverrides?: Partial<MiscTerminalsDep>
|
|
97
|
+
agentOverrides?: Partial<MiscAgentDep>
|
|
98
|
+
sessionShare?: MiscSessionShareDep | null
|
|
99
|
+
analyticsOverrides?: Partial<MiscAnalyticsDep>
|
|
100
|
+
worktreeResult?: unknown[]
|
|
101
|
+
originHost?: string
|
|
102
|
+
} = {}): TestDeps {
|
|
103
|
+
const sent: unknown[] = []
|
|
104
|
+
const sideBroadcasts: number[] = []
|
|
105
|
+
const chatBroadcasts: string[] = []
|
|
106
|
+
const pushSnapshots: string[] = []
|
|
107
|
+
const worktreeCallPaths: string[] = []
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
store: makeStore(opts.storeOverrides),
|
|
111
|
+
terminals: makeTerminals(opts.terminalsOverrides),
|
|
112
|
+
agent: makeAgent(opts.agentOverrides),
|
|
113
|
+
sessionShare: opts.sessionShare !== undefined ? opts.sessionShare : null,
|
|
114
|
+
analytics: { track: mock(() => {}) },
|
|
115
|
+
listWorktrees: async (path) => {
|
|
116
|
+
worktreeCallPaths.push(path)
|
|
117
|
+
return (opts.worktreeResult ?? []) as import("../shared/types").GitWorktree[]
|
|
118
|
+
},
|
|
119
|
+
getOriginHost: () => opts.originHost ?? "http://localhost:3000",
|
|
120
|
+
send: (envelope: ServerEnvelope) => { sent.push(envelope) },
|
|
121
|
+
broadcastSidebar: async () => { sideBroadcasts.push(1) },
|
|
122
|
+
broadcastChatAndSidebar: async (chatId) => { chatBroadcasts.push(chatId) },
|
|
123
|
+
pushTerminalSnapshot: (terminalId) => { pushSnapshots.push(terminalId) },
|
|
124
|
+
sent,
|
|
125
|
+
get sideBroadcasts() { return sideBroadcasts.length },
|
|
126
|
+
chatBroadcasts,
|
|
127
|
+
pushSnapshots,
|
|
128
|
+
worktreeCallPaths,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Unknown command
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
describe("handleMiscCommand", () => {
|
|
137
|
+
test("returns false for an unrelated command type", async () => {
|
|
138
|
+
const deps = makeDeps()
|
|
139
|
+
const handled = await handleMiscCommand(
|
|
140
|
+
deps,
|
|
141
|
+
{ type: "settings.readAppSettings" } as unknown as ClientCommand,
|
|
142
|
+
"r0",
|
|
143
|
+
)
|
|
144
|
+
expect(handled).toBe(false)
|
|
145
|
+
expect(deps.sent).toHaveLength(0)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// message.*
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
test("message.enqueue — calls agent.enqueue, acks with result, broadcasts chat+sidebar", async () => {
|
|
153
|
+
const deps = makeDeps({ agentOverrides: { enqueue: mock(async () => ({ queuedMessageId: "q-1" })) } })
|
|
154
|
+
const cmd: ClientCommand = {
|
|
155
|
+
type: "message.enqueue",
|
|
156
|
+
chatId: "chat-1",
|
|
157
|
+
content: "Hello",
|
|
158
|
+
attachments: [],
|
|
159
|
+
}
|
|
160
|
+
const handled = await handleMiscCommand(deps, cmd, "r1")
|
|
161
|
+
expect(handled).toBe(true)
|
|
162
|
+
expect(deps.agent.enqueue as ReturnType<typeof mock>).toHaveBeenCalledWith(cmd)
|
|
163
|
+
const ack = deps.sent[0] as { type: string; result: unknown }
|
|
164
|
+
expect(ack.type).toBe("ack")
|
|
165
|
+
expect(ack.result).toEqual({ queuedMessageId: "q-1" })
|
|
166
|
+
expect(deps.chatBroadcasts).toContain("chat-1")
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
test("message.steer — calls agent.steer, acks, broadcasts chat+sidebar", async () => {
|
|
170
|
+
const deps = makeDeps()
|
|
171
|
+
const cmd: ClientCommand = {
|
|
172
|
+
type: "message.steer",
|
|
173
|
+
chatId: "chat-2",
|
|
174
|
+
queuedMessageId: "q-2",
|
|
175
|
+
}
|
|
176
|
+
const handled = await handleMiscCommand(deps, cmd, "r2")
|
|
177
|
+
expect(handled).toBe(true)
|
|
178
|
+
expect(deps.agent.steer as ReturnType<typeof mock>).toHaveBeenCalledWith(cmd)
|
|
179
|
+
expect((deps.sent[0] as { type: string }).type).toBe("ack")
|
|
180
|
+
expect(deps.chatBroadcasts).toContain("chat-2")
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test("message.dequeue — calls agent.dequeue, acks, broadcasts chat+sidebar", async () => {
|
|
184
|
+
const deps = makeDeps()
|
|
185
|
+
const cmd: ClientCommand = {
|
|
186
|
+
type: "message.dequeue",
|
|
187
|
+
chatId: "chat-3",
|
|
188
|
+
queuedMessageId: "q-3",
|
|
189
|
+
}
|
|
190
|
+
const handled = await handleMiscCommand(deps, cmd, "r3")
|
|
191
|
+
expect(handled).toBe(true)
|
|
192
|
+
expect(deps.agent.dequeue as ReturnType<typeof mock>).toHaveBeenCalledWith(cmd)
|
|
193
|
+
expect((deps.sent[0] as { type: string }).type).toBe("ack")
|
|
194
|
+
expect(deps.chatBroadcasts).toContain("chat-3")
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// terminal.*
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
test("terminal.create — calls createTerminal with project path, acks with snapshot", async () => {
|
|
202
|
+
const deps = makeDeps({
|
|
203
|
+
storeOverrides: {
|
|
204
|
+
getProject: mock(() => ({ localPath: "/home/user/project" })),
|
|
205
|
+
},
|
|
206
|
+
})
|
|
207
|
+
const cmd: ClientCommand = {
|
|
208
|
+
type: "terminal.create",
|
|
209
|
+
projectId: "proj-1",
|
|
210
|
+
terminalId: "term-1",
|
|
211
|
+
cols: 120,
|
|
212
|
+
rows: 40,
|
|
213
|
+
scrollback: 5000,
|
|
214
|
+
}
|
|
215
|
+
const handled = await handleMiscCommand(deps, cmd, "r4")
|
|
216
|
+
expect(handled).toBe(true)
|
|
217
|
+
const createFn = deps.terminals.createTerminal as ReturnType<typeof mock>
|
|
218
|
+
expect(createFn).toHaveBeenCalledWith({
|
|
219
|
+
projectPath: "/home/user/project",
|
|
220
|
+
terminalId: "term-1",
|
|
221
|
+
cols: 120,
|
|
222
|
+
rows: 40,
|
|
223
|
+
scrollback: 5000,
|
|
224
|
+
})
|
|
225
|
+
const ack = deps.sent[0] as { type: string; result: unknown }
|
|
226
|
+
expect(ack.type).toBe("ack")
|
|
227
|
+
expect(ack.result).toBeDefined()
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
test("terminal.create — throws when project not found", async () => {
|
|
231
|
+
const deps = makeDeps({ storeOverrides: { getProject: mock(() => null) } })
|
|
232
|
+
const cmd: ClientCommand = {
|
|
233
|
+
type: "terminal.create",
|
|
234
|
+
projectId: "missing",
|
|
235
|
+
terminalId: "term-x",
|
|
236
|
+
cols: 80,
|
|
237
|
+
rows: 24,
|
|
238
|
+
scrollback: 1000,
|
|
239
|
+
}
|
|
240
|
+
await expect(handleMiscCommand(deps, cmd, "r5")).rejects.toThrow("Project not found")
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test("terminal.input — calls terminals.write, acks", async () => {
|
|
244
|
+
const deps = makeDeps()
|
|
245
|
+
const cmd: ClientCommand = { type: "terminal.input", terminalId: "term-1", data: "ls\r" }
|
|
246
|
+
const handled = await handleMiscCommand(deps, cmd, "r6")
|
|
247
|
+
expect(handled).toBe(true)
|
|
248
|
+
expect(deps.terminals.write as ReturnType<typeof mock>).toHaveBeenCalledWith("term-1", "ls\r")
|
|
249
|
+
expect((deps.sent[0] as { type: string }).type).toBe("ack")
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
test("terminal.resize — calls terminals.resize, acks", async () => {
|
|
253
|
+
const deps = makeDeps()
|
|
254
|
+
const cmd: ClientCommand = { type: "terminal.resize", terminalId: "term-1", cols: 200, rows: 50 }
|
|
255
|
+
const handled = await handleMiscCommand(deps, cmd, "r7")
|
|
256
|
+
expect(handled).toBe(true)
|
|
257
|
+
expect(deps.terminals.resize as ReturnType<typeof mock>).toHaveBeenCalledWith("term-1", 200, 50)
|
|
258
|
+
expect((deps.sent[0] as { type: string }).type).toBe("ack")
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
test("terminal.close — calls terminals.close, acks, then pushes terminal snapshot", async () => {
|
|
262
|
+
const deps = makeDeps()
|
|
263
|
+
const cmd: ClientCommand = { type: "terminal.close", terminalId: "term-2" }
|
|
264
|
+
const handled = await handleMiscCommand(deps, cmd, "r8")
|
|
265
|
+
expect(handled).toBe(true)
|
|
266
|
+
expect(deps.terminals.close as ReturnType<typeof mock>).toHaveBeenCalledWith("term-2")
|
|
267
|
+
expect((deps.sent[0] as { type: string }).type).toBe("ack")
|
|
268
|
+
expect(deps.pushSnapshots).toContain("term-2")
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
// stack.*
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
test("stack.create — creates stack, acks with stackId, tracks analytics, broadcasts sidebar", async () => {
|
|
276
|
+
const deps = makeDeps({
|
|
277
|
+
storeOverrides: { createStack: mock(async () => ({ id: "stack-99" })) },
|
|
278
|
+
})
|
|
279
|
+
const cmd: ClientCommand = { type: "stack.create", title: "My Stack", projectIds: ["p1", "p2"] }
|
|
280
|
+
const handled = await handleMiscCommand(deps, cmd, "r9")
|
|
281
|
+
expect(handled).toBe(true)
|
|
282
|
+
expect(deps.store.createStack as ReturnType<typeof mock>).toHaveBeenCalledWith("My Stack", ["p1", "p2"])
|
|
283
|
+
const ack = deps.sent[0] as { type: string; result: { stackId: string } }
|
|
284
|
+
expect(ack.result).toEqual({ stackId: "stack-99" })
|
|
285
|
+
expect(deps.analytics.track as ReturnType<typeof mock>).toHaveBeenCalledWith("stack_created")
|
|
286
|
+
expect(deps.sideBroadcasts).toBeGreaterThan(0)
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
test("stack.rename — renames, acks, broadcasts sidebar", async () => {
|
|
290
|
+
const deps = makeDeps()
|
|
291
|
+
const cmd: ClientCommand = { type: "stack.rename", stackId: "s-1", title: "New Title" }
|
|
292
|
+
const handled = await handleMiscCommand(deps, cmd, "r10")
|
|
293
|
+
expect(handled).toBe(true)
|
|
294
|
+
expect(deps.store.renameStack as ReturnType<typeof mock>).toHaveBeenCalledWith("s-1", "New Title")
|
|
295
|
+
expect((deps.sent[0] as { type: string }).type).toBe("ack")
|
|
296
|
+
expect(deps.sideBroadcasts).toBeGreaterThan(0)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
test("stack.remove — removes, acks, broadcasts sidebar", async () => {
|
|
300
|
+
const deps = makeDeps()
|
|
301
|
+
const cmd: ClientCommand = { type: "stack.remove", stackId: "s-2" }
|
|
302
|
+
const handled = await handleMiscCommand(deps, cmd, "r11")
|
|
303
|
+
expect(handled).toBe(true)
|
|
304
|
+
expect(deps.store.removeStack as ReturnType<typeof mock>).toHaveBeenCalledWith("s-2")
|
|
305
|
+
expect((deps.sent[0] as { type: string }).type).toBe("ack")
|
|
306
|
+
expect(deps.sideBroadcasts).toBeGreaterThan(0)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
test("stack.addProject — adds project, acks, broadcasts sidebar", async () => {
|
|
310
|
+
const deps = makeDeps()
|
|
311
|
+
const cmd: ClientCommand = { type: "stack.addProject", stackId: "s-3", projectId: "proj-5" }
|
|
312
|
+
const handled = await handleMiscCommand(deps, cmd, "r12")
|
|
313
|
+
expect(handled).toBe(true)
|
|
314
|
+
expect(deps.store.addProjectToStack as ReturnType<typeof mock>).toHaveBeenCalledWith("s-3", "proj-5")
|
|
315
|
+
expect((deps.sent[0] as { type: string }).type).toBe("ack")
|
|
316
|
+
expect(deps.sideBroadcasts).toBeGreaterThan(0)
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
test("stack.removeProject — removes project, acks, broadcasts sidebar", async () => {
|
|
320
|
+
const deps = makeDeps()
|
|
321
|
+
const cmd: ClientCommand = { type: "stack.removeProject", stackId: "s-4", projectId: "proj-6" }
|
|
322
|
+
const handled = await handleMiscCommand(deps, cmd, "r13")
|
|
323
|
+
expect(handled).toBe(true)
|
|
324
|
+
expect(deps.store.removeProjectFromStack as ReturnType<typeof mock>).toHaveBeenCalledWith("s-4", "proj-6")
|
|
325
|
+
expect((deps.sent[0] as { type: string }).type).toBe("ack")
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
test("stack.listWorktrees — lists worktrees for project path, acks with result", async () => {
|
|
329
|
+
const fakeWorktrees = [{ branch: "main", path: "/repo" }]
|
|
330
|
+
const deps = makeDeps({
|
|
331
|
+
storeOverrides: { getProject: mock(() => ({ localPath: "/my/repo" })) },
|
|
332
|
+
worktreeResult: fakeWorktrees,
|
|
333
|
+
})
|
|
334
|
+
const cmd: ClientCommand = { type: "stack.listWorktrees", projectId: "proj-w" }
|
|
335
|
+
const handled = await handleMiscCommand(deps, cmd, "r14")
|
|
336
|
+
expect(handled).toBe(true)
|
|
337
|
+
expect(deps.worktreeCallPaths).toContain("/my/repo")
|
|
338
|
+
const ack = deps.sent[0] as { type: string; result: { worktrees: unknown[] } }
|
|
339
|
+
expect(ack.result.worktrees).toEqual(fakeWorktrees)
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
test("stack.listWorktrees — throws when project not found", async () => {
|
|
343
|
+
const deps = makeDeps({ storeOverrides: { getProject: mock(() => null) } })
|
|
344
|
+
const cmd: ClientCommand = { type: "stack.listWorktrees", projectId: "missing" }
|
|
345
|
+
await expect(handleMiscCommand(deps, cmd, "r15")).rejects.toThrow("Project not found")
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
// share.*
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
test("share.mint — mints token, acks with ok result when service available", async () => {
|
|
353
|
+
const shareService = makeSessionShare()
|
|
354
|
+
const deps = makeDeps({ sessionShare: shareService })
|
|
355
|
+
const cmd: ClientCommand = {
|
|
356
|
+
type: "share.mint",
|
|
357
|
+
payload: { chatId: "chat-share-1", ttlHours: 3600 },
|
|
358
|
+
}
|
|
359
|
+
const handled = await handleMiscCommand(deps, cmd, "r16")
|
|
360
|
+
expect(handled).toBe(true)
|
|
361
|
+
expect(shareService.mintToken as ReturnType<typeof mock>).toHaveBeenCalledWith(
|
|
362
|
+
{ chatId: "chat-share-1", ttlHours: 3600 },
|
|
363
|
+
"http://localhost:3000",
|
|
364
|
+
)
|
|
365
|
+
const ack = deps.sent[0] as { type: string; result: { ok: boolean; kind?: string } }
|
|
366
|
+
expect(ack.result.ok).toBe(true)
|
|
367
|
+
expect(ack.result.kind).toBe("mint")
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
test("share.mint — acks with unavailable error when no sessionShare", async () => {
|
|
371
|
+
const deps = makeDeps({ sessionShare: null })
|
|
372
|
+
const cmd: ClientCommand = { type: "share.mint", payload: { chatId: "c-1", ttlHours: 60 } }
|
|
373
|
+
const handled = await handleMiscCommand(deps, cmd, "r17")
|
|
374
|
+
expect(handled).toBe(true)
|
|
375
|
+
const ack = deps.sent[0] as { type: string; result: { ok: boolean; error?: unknown } }
|
|
376
|
+
expect(ack.result.ok).toBe(false)
|
|
377
|
+
expect(ack.result.error).toBeDefined()
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
test("share.revoke — revokes token, acks with ok result when service available", async () => {
|
|
381
|
+
const shareService = makeSessionShare()
|
|
382
|
+
const deps = makeDeps({ sessionShare: shareService })
|
|
383
|
+
const cmd: ClientCommand = { type: "share.revoke", payload: { tokenId: "tok-1" } }
|
|
384
|
+
const handled = await handleMiscCommand(deps, cmd, "r18")
|
|
385
|
+
expect(handled).toBe(true)
|
|
386
|
+
const ack = deps.sent[0] as { type: string; result: { ok: boolean; kind?: string } }
|
|
387
|
+
expect(ack.result.ok).toBe(true)
|
|
388
|
+
expect(ack.result.kind).toBe("revoke")
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
test("share.revoke — acks with not_found error when no sessionShare", async () => {
|
|
392
|
+
const deps = makeDeps({ sessionShare: null })
|
|
393
|
+
const cmd: ClientCommand = { type: "share.revoke", payload: { tokenId: "tok-x" } }
|
|
394
|
+
const handled = await handleMiscCommand(deps, cmd, "r19")
|
|
395
|
+
expect(handled).toBe(true)
|
|
396
|
+
const ack = deps.sent[0] as { type: string; result: { ok: boolean } }
|
|
397
|
+
expect(ack.result.ok).toBe(false)
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
test("share.list — lists shares for chat when service available", async () => {
|
|
401
|
+
const shareService = makeSessionShare({ listSharesForChat: mock(() => [{ tokenId: "t-1", chatId: "chat-s", createdAt: 0, expiresAt: 0, url: "https://u", revoked: false }]) })
|
|
402
|
+
const deps = makeDeps({ sessionShare: shareService, originHost: "https://myapp.com" })
|
|
403
|
+
const cmd: ClientCommand = { type: "share.list", payload: { chatId: "chat-s" } }
|
|
404
|
+
const handled = await handleMiscCommand(deps, cmd, "r20")
|
|
405
|
+
expect(handled).toBe(true)
|
|
406
|
+
expect(shareService.listSharesForChat as ReturnType<typeof mock>).toHaveBeenCalledWith("chat-s", "https://myapp.com")
|
|
407
|
+
const ack = deps.sent[0] as { type: string; result: { ok: boolean; kind: string; data: { shares: unknown[] } } }
|
|
408
|
+
expect(ack.result.ok).toBe(true)
|
|
409
|
+
expect(ack.result.kind).toBe("list")
|
|
410
|
+
expect(ack.result.data.shares).toHaveLength(1)
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
test("share.list — acks with empty shares when no sessionShare", async () => {
|
|
414
|
+
const deps = makeDeps({ sessionShare: null })
|
|
415
|
+
const cmd: ClientCommand = { type: "share.list", payload: { chatId: "chat-empty" } }
|
|
416
|
+
const handled = await handleMiscCommand(deps, cmd, "r21")
|
|
417
|
+
expect(handled).toBe(true)
|
|
418
|
+
const ack = deps.sent[0] as { type: string; result: { ok: boolean; data: { shares: unknown[] } } }
|
|
419
|
+
expect(ack.result.ok).toBe(true)
|
|
420
|
+
expect(ack.result.data.shares).toHaveLength(0)
|
|
421
|
+
})
|
|
422
|
+
})
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ws-router-misc.ts
|
|
3
|
+
*
|
|
4
|
+
* WS command handlers for terminal, message-queue, stack, and share operations:
|
|
5
|
+
* terminal.create, terminal.input, terminal.resize, terminal.close
|
|
6
|
+
* message.enqueue, message.steer, message.dequeue
|
|
7
|
+
* stack.create, stack.rename, stack.remove, stack.addProject,
|
|
8
|
+
* stack.removeProject, stack.listWorktrees
|
|
9
|
+
* share.mint, share.revoke, share.list
|
|
10
|
+
*
|
|
11
|
+
* Extracted from ws-router.ts.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { PROTOCOL_VERSION } from "../shared/types"
|
|
15
|
+
import type { GitWorktree } from "../shared/types"
|
|
16
|
+
import type { ClientCommand, ServerEnvelope } from "../shared/protocol"
|
|
17
|
+
import type { TerminalSnapshot } from "../shared/protocol"
|
|
18
|
+
import type { ShareCommandResult } from "../shared/session-share/protocol"
|
|
19
|
+
import type { MintRequest, MintResponse, RevokeRequest, ShareError, ShareSummary } from "../shared/session-share/types"
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Dep interfaces (duck-typed; avoids circular imports with ws-router.ts)
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/** The subset of EventStore methods consumed by misc WS commands. */
|
|
26
|
+
export interface MiscStoreDep {
|
|
27
|
+
getProject(id: string): { localPath: string } | undefined | null
|
|
28
|
+
createStack(title: string, projectIds: string[]): Promise<{ id: string }>
|
|
29
|
+
renameStack(stackId: string, title: string): Promise<void>
|
|
30
|
+
removeStack(stackId: string): Promise<void>
|
|
31
|
+
addProjectToStack(stackId: string, projectId: string): Promise<void>
|
|
32
|
+
removeProjectFromStack(stackId: string, projectId: string): Promise<void>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The subset of TerminalManager methods consumed by misc WS commands. */
|
|
36
|
+
export interface MiscTerminalsDep {
|
|
37
|
+
createTerminal(args: {
|
|
38
|
+
projectPath: string
|
|
39
|
+
terminalId: string
|
|
40
|
+
cols: number
|
|
41
|
+
rows: number
|
|
42
|
+
scrollback: number
|
|
43
|
+
}): TerminalSnapshot
|
|
44
|
+
write(terminalId: string, data: string): void
|
|
45
|
+
resize(terminalId: string, cols: number, rows: number): void
|
|
46
|
+
close(terminalId: string): void
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The subset of AgentCoordinator methods consumed by misc WS commands. */
|
|
50
|
+
export interface MiscAgentDep {
|
|
51
|
+
enqueue(command: Extract<ClientCommand, { type: "message.enqueue" }>): Promise<{ queuedMessageId: string }>
|
|
52
|
+
steer(command: Extract<ClientCommand, { type: "message.steer" }>): Promise<void>
|
|
53
|
+
dequeue(command: Extract<ClientCommand, { type: "message.dequeue" }>): Promise<void>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The subset of AnalyticsReporter consumed by misc WS commands. */
|
|
57
|
+
export interface MiscAnalyticsDep {
|
|
58
|
+
track(event: string, props?: Record<string, unknown>): void
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Shared result wrapper used by SessionShareService methods. */
|
|
62
|
+
export type ShareResult<T> = { ok: true; data: T } | { ok: false; error: ShareError }
|
|
63
|
+
|
|
64
|
+
/** The subset of SessionShareService consumed by misc WS commands. */
|
|
65
|
+
export interface MiscSessionShareDep {
|
|
66
|
+
mintToken(req: MintRequest, baseUrl: string): Promise<ShareResult<MintResponse>>
|
|
67
|
+
revokeToken(req: RevokeRequest): Promise<ShareResult<{ tokenId: string }>>
|
|
68
|
+
listSharesForChat(chatId: string, baseUrl: string): ShareSummary[]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface MiscCommandDeps {
|
|
72
|
+
/** EventStore methods for project/stack operations. */
|
|
73
|
+
store: MiscStoreDep
|
|
74
|
+
/** TerminalManager for terminal lifecycle. */
|
|
75
|
+
terminals: MiscTerminalsDep
|
|
76
|
+
/** AgentCoordinator for message-queue operations. */
|
|
77
|
+
agent: MiscAgentDep
|
|
78
|
+
/** Optional session-share service (feature-gated). */
|
|
79
|
+
sessionShare?: MiscSessionShareDep | null
|
|
80
|
+
/** Analytics reporter. */
|
|
81
|
+
analytics: MiscAnalyticsDep
|
|
82
|
+
/**
|
|
83
|
+
* List the worktrees for a given repo root.
|
|
84
|
+
* Injected to avoid a direct dependency on the IO adapter.
|
|
85
|
+
*/
|
|
86
|
+
listWorktrees(repoPath: string): Promise<GitWorktree[]>
|
|
87
|
+
/** Return the origin host for the current WebSocket connection. */
|
|
88
|
+
getOriginHost(): string
|
|
89
|
+
/** Pre-bound to the current WebSocket; called to send an ack envelope. */
|
|
90
|
+
send(envelope: ServerEnvelope): void
|
|
91
|
+
/** Broadcast sidebar snapshot to all connected clients. */
|
|
92
|
+
broadcastSidebar(): Promise<void>
|
|
93
|
+
/** Broadcast chat + sidebar snapshots to all connected clients. */
|
|
94
|
+
broadcastChatAndSidebar(chatId: string): Promise<void>
|
|
95
|
+
/**
|
|
96
|
+
* Push a fresh terminal snapshot to subscribers of the given terminal.
|
|
97
|
+
* Called after terminal.close to reflect the updated status.
|
|
98
|
+
*/
|
|
99
|
+
pushTerminalSnapshot(terminalId: string): void
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Command dispatcher
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Handle one misc WS command (terminal/message/stack/share group).
|
|
108
|
+
*
|
|
109
|
+
* Returns `true` when the command was handled (caller should `return`).
|
|
110
|
+
* Returns `false` when the command type is outside this module's scope.
|
|
111
|
+
*/
|
|
112
|
+
export async function handleMiscCommand(
|
|
113
|
+
deps: MiscCommandDeps,
|
|
114
|
+
command: ClientCommand,
|
|
115
|
+
id: string,
|
|
116
|
+
): Promise<boolean> {
|
|
117
|
+
const { store, terminals, agent, sessionShare, analytics, send, broadcastSidebar, broadcastChatAndSidebar, pushTerminalSnapshot, listWorktrees, getOriginHost } = deps
|
|
118
|
+
|
|
119
|
+
switch (command.type) {
|
|
120
|
+
// ------------------------------------------------------------------
|
|
121
|
+
// message.*
|
|
122
|
+
// ------------------------------------------------------------------
|
|
123
|
+
case "message.enqueue": {
|
|
124
|
+
const result = await agent.enqueue(command)
|
|
125
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
126
|
+
await broadcastChatAndSidebar(command.chatId)
|
|
127
|
+
return true
|
|
128
|
+
}
|
|
129
|
+
case "message.steer": {
|
|
130
|
+
await agent.steer(command)
|
|
131
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id })
|
|
132
|
+
await broadcastChatAndSidebar(command.chatId)
|
|
133
|
+
return true
|
|
134
|
+
}
|
|
135
|
+
case "message.dequeue": {
|
|
136
|
+
await agent.dequeue(command)
|
|
137
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id })
|
|
138
|
+
await broadcastChatAndSidebar(command.chatId)
|
|
139
|
+
return true
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ------------------------------------------------------------------
|
|
143
|
+
// terminal.*
|
|
144
|
+
// ------------------------------------------------------------------
|
|
145
|
+
case "terminal.create": {
|
|
146
|
+
const project = store.getProject(command.projectId)
|
|
147
|
+
if (!project) {
|
|
148
|
+
throw new Error("Project not found")
|
|
149
|
+
}
|
|
150
|
+
const snapshot = terminals.createTerminal({
|
|
151
|
+
projectPath: project.localPath,
|
|
152
|
+
terminalId: command.terminalId,
|
|
153
|
+
cols: command.cols,
|
|
154
|
+
rows: command.rows,
|
|
155
|
+
scrollback: command.scrollback,
|
|
156
|
+
})
|
|
157
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: snapshot })
|
|
158
|
+
return true
|
|
159
|
+
}
|
|
160
|
+
case "terminal.input": {
|
|
161
|
+
terminals.write(command.terminalId, command.data)
|
|
162
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id })
|
|
163
|
+
return true
|
|
164
|
+
}
|
|
165
|
+
case "terminal.resize": {
|
|
166
|
+
terminals.resize(command.terminalId, command.cols, command.rows)
|
|
167
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id })
|
|
168
|
+
return true
|
|
169
|
+
}
|
|
170
|
+
case "terminal.close": {
|
|
171
|
+
terminals.close(command.terminalId)
|
|
172
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id })
|
|
173
|
+
pushTerminalSnapshot(command.terminalId)
|
|
174
|
+
return true
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ------------------------------------------------------------------
|
|
178
|
+
// stack.*
|
|
179
|
+
// ------------------------------------------------------------------
|
|
180
|
+
case "stack.create": {
|
|
181
|
+
const stack = await store.createStack(command.title, command.projectIds)
|
|
182
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: { stackId: stack.id } })
|
|
183
|
+
analytics.track("stack_created")
|
|
184
|
+
await broadcastSidebar()
|
|
185
|
+
return true
|
|
186
|
+
}
|
|
187
|
+
case "stack.rename": {
|
|
188
|
+
await store.renameStack(command.stackId, command.title)
|
|
189
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id })
|
|
190
|
+
await broadcastSidebar()
|
|
191
|
+
return true
|
|
192
|
+
}
|
|
193
|
+
case "stack.remove": {
|
|
194
|
+
await store.removeStack(command.stackId)
|
|
195
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id })
|
|
196
|
+
await broadcastSidebar()
|
|
197
|
+
return true
|
|
198
|
+
}
|
|
199
|
+
case "stack.addProject": {
|
|
200
|
+
await store.addProjectToStack(command.stackId, command.projectId)
|
|
201
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id })
|
|
202
|
+
await broadcastSidebar()
|
|
203
|
+
return true
|
|
204
|
+
}
|
|
205
|
+
case "stack.removeProject": {
|
|
206
|
+
await store.removeProjectFromStack(command.stackId, command.projectId)
|
|
207
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id })
|
|
208
|
+
await broadcastSidebar()
|
|
209
|
+
return true
|
|
210
|
+
}
|
|
211
|
+
case "stack.listWorktrees": {
|
|
212
|
+
const project = store.getProject(command.projectId)
|
|
213
|
+
if (!project) {
|
|
214
|
+
throw new Error("Project not found")
|
|
215
|
+
}
|
|
216
|
+
const worktrees = await listWorktrees(project.localPath)
|
|
217
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: { worktrees } })
|
|
218
|
+
return true
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ------------------------------------------------------------------
|
|
222
|
+
// share.*
|
|
223
|
+
// ------------------------------------------------------------------
|
|
224
|
+
case "share.mint": {
|
|
225
|
+
if (!sessionShare) {
|
|
226
|
+
const noSvcResult: ShareCommandResult = { ok: false, error: { kind: "snapshot_write_failed", message: "session-share service unavailable" } }
|
|
227
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: noSvcResult })
|
|
228
|
+
return true
|
|
229
|
+
}
|
|
230
|
+
const r = await sessionShare.mintToken(command.payload, getOriginHost())
|
|
231
|
+
const result: ShareCommandResult = r.ok
|
|
232
|
+
? { ok: true, kind: "mint", data: r.data }
|
|
233
|
+
: { ok: false, error: r.error }
|
|
234
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
235
|
+
return true
|
|
236
|
+
}
|
|
237
|
+
case "share.revoke": {
|
|
238
|
+
if (!sessionShare) {
|
|
239
|
+
const noSvcResult: ShareCommandResult = { ok: false, error: { kind: "not_found" } }
|
|
240
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: noSvcResult })
|
|
241
|
+
return true
|
|
242
|
+
}
|
|
243
|
+
const r = await sessionShare.revokeToken(command.payload)
|
|
244
|
+
const result: ShareCommandResult = r.ok
|
|
245
|
+
? { ok: true, kind: "revoke", data: r.data }
|
|
246
|
+
: { ok: false, error: r.error }
|
|
247
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
248
|
+
return true
|
|
249
|
+
}
|
|
250
|
+
case "share.list": {
|
|
251
|
+
if (!sessionShare) {
|
|
252
|
+
const emptyResult: ShareCommandResult = { ok: true, kind: "list", data: { shares: [] } }
|
|
253
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: emptyResult })
|
|
254
|
+
return true
|
|
255
|
+
}
|
|
256
|
+
const shares = sessionShare.listSharesForChat(command.payload.chatId, getOriginHost())
|
|
257
|
+
const listResult: ShareCommandResult = { ok: true, kind: "list", data: { shares } }
|
|
258
|
+
send({ v: PROTOCOL_VERSION, type: "ack", id, result: listResult })
|
|
259
|
+
return true
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
default:
|
|
263
|
+
return false
|
|
264
|
+
}
|
|
265
|
+
}
|