codekanban 0.29.0 → 0.30.0
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.
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
CodeKanbanConfigError,
|
|
3
|
+
CodeKanbanError,
|
|
4
|
+
CodeKanbanValidationError,
|
|
5
|
+
} from "./errors.js";
|
|
2
6
|
import {
|
|
3
7
|
WEB_SESSION_PROTOCOL_VERSION,
|
|
4
8
|
buildWebSessionCommandFrame,
|
|
@@ -6,8 +10,12 @@ import {
|
|
|
6
10
|
decodeWebSessionSocketMessage,
|
|
7
11
|
isWebSessionHeartbeatFrame,
|
|
8
12
|
normalizeWebSessionFrame,
|
|
9
|
-
} from
|
|
10
|
-
import {
|
|
13
|
+
} from "./web-session-shared.js";
|
|
14
|
+
import {
|
|
15
|
+
ensureArrayOfStrings,
|
|
16
|
+
ensureOptionalString,
|
|
17
|
+
ensureString,
|
|
18
|
+
} from "./utils.js";
|
|
11
19
|
|
|
12
20
|
const SOCKET_OPEN = 1;
|
|
13
21
|
const DEFAULT_OPEN_TIMEOUT_MS = 8000;
|
|
@@ -27,10 +35,12 @@ function normalizeHistoryLimit(value) {
|
|
|
27
35
|
|
|
28
36
|
export class WebSessionCommandChannel {
|
|
29
37
|
constructor({ url, WebSocketImpl } = {}) {
|
|
30
|
-
const resolvedUrl = ensureString(url,
|
|
38
|
+
const resolvedUrl = ensureString(url, "url");
|
|
31
39
|
const Socket = WebSocketImpl || globalThis.WebSocket;
|
|
32
40
|
if (!Socket) {
|
|
33
|
-
throw new CodeKanbanConfigError(
|
|
41
|
+
throw new CodeKanbanConfigError(
|
|
42
|
+
"WebSocket implementation is unavailable",
|
|
43
|
+
);
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
this.url = resolvedUrl;
|
|
@@ -45,23 +55,28 @@ export class WebSessionCommandChannel {
|
|
|
45
55
|
});
|
|
46
56
|
this._closed = false;
|
|
47
57
|
|
|
48
|
-
this.socket.addEventListener(
|
|
58
|
+
this.socket.addEventListener("open", () => {
|
|
49
59
|
this._resolveOpen?.();
|
|
50
60
|
});
|
|
51
61
|
|
|
52
|
-
this.socket.addEventListener(
|
|
62
|
+
this.socket.addEventListener("message", (event) => {
|
|
53
63
|
this._handleMessage(event.data);
|
|
54
64
|
});
|
|
55
65
|
|
|
56
|
-
this.socket.addEventListener(
|
|
57
|
-
const error = new CodeKanbanError(
|
|
66
|
+
this.socket.addEventListener("error", (event) => {
|
|
67
|
+
const error = new CodeKanbanError("web session command websocket error", {
|
|
68
|
+
event,
|
|
69
|
+
});
|
|
58
70
|
this._rejectOpen?.(error);
|
|
59
71
|
this._rejectAll(error);
|
|
60
72
|
});
|
|
61
73
|
|
|
62
|
-
this.socket.addEventListener(
|
|
74
|
+
this.socket.addEventListener("close", (event) => {
|
|
63
75
|
this._closed = true;
|
|
64
|
-
const error = new CodeKanbanError(
|
|
76
|
+
const error = new CodeKanbanError(
|
|
77
|
+
"web session command websocket closed",
|
|
78
|
+
{ event },
|
|
79
|
+
);
|
|
65
80
|
this._rejectOpen?.(error);
|
|
66
81
|
this._rejectAll(error);
|
|
67
82
|
});
|
|
@@ -75,7 +90,12 @@ export class WebSessionCommandChannel {
|
|
|
75
90
|
this._openPromise,
|
|
76
91
|
new Promise((_, reject) => {
|
|
77
92
|
setTimeout(
|
|
78
|
-
() =>
|
|
93
|
+
() =>
|
|
94
|
+
reject(
|
|
95
|
+
new CodeKanbanValidationError(
|
|
96
|
+
`web session command channel did not open within ${timeoutMs}ms`,
|
|
97
|
+
),
|
|
98
|
+
),
|
|
79
99
|
timeoutMs,
|
|
80
100
|
);
|
|
81
101
|
}),
|
|
@@ -84,9 +104,9 @@ export class WebSessionCommandChannel {
|
|
|
84
104
|
|
|
85
105
|
async list(projectId) {
|
|
86
106
|
const ack = await this._executeCommand({
|
|
87
|
-
operation:
|
|
107
|
+
operation: "list",
|
|
88
108
|
payload: {
|
|
89
|
-
pid: ensureString(projectId,
|
|
109
|
+
pid: ensureString(projectId, "projectId"),
|
|
90
110
|
},
|
|
91
111
|
});
|
|
92
112
|
return Array.isArray(ack.payload?.items) ? ack.payload.items : [];
|
|
@@ -94,11 +114,11 @@ export class WebSessionCommandChannel {
|
|
|
94
114
|
|
|
95
115
|
async create(input = {}) {
|
|
96
116
|
const { frame } = await this._executeCommand({
|
|
97
|
-
operation:
|
|
117
|
+
operation: "create",
|
|
98
118
|
payload: {
|
|
99
|
-
pid: ensureString(input.projectId,
|
|
119
|
+
pid: ensureString(input.projectId, "projectId"),
|
|
100
120
|
wid: ensureOptionalString(input.worktreeId),
|
|
101
|
-
ag: ensureString(input.agent,
|
|
121
|
+
ag: ensureString(input.agent, "agent"),
|
|
102
122
|
md: ensureOptionalString(input.model),
|
|
103
123
|
re: ensureOptionalString(input.reasoningEffort),
|
|
104
124
|
wm: ensureOptionalString(input.workflowMode),
|
|
@@ -106,135 +126,153 @@ export class WebSessionCommandChannel {
|
|
|
106
126
|
pm: ensureOptionalString(input.permissionMode),
|
|
107
127
|
ttl: ensureOptionalString(input.title),
|
|
108
128
|
},
|
|
109
|
-
expectType:
|
|
129
|
+
expectType: "snapshot",
|
|
110
130
|
});
|
|
111
131
|
return frame.snapshot;
|
|
112
132
|
}
|
|
113
133
|
|
|
114
134
|
async connect(sessionId) {
|
|
115
135
|
const { frame } = await this._executeCommand({
|
|
116
|
-
operation:
|
|
136
|
+
operation: "connect",
|
|
117
137
|
sessionId,
|
|
118
|
-
expectType:
|
|
138
|
+
expectType: "snapshot",
|
|
119
139
|
});
|
|
120
140
|
return frame.snapshot;
|
|
121
141
|
}
|
|
122
142
|
|
|
123
143
|
async history(sessionId, options = {}) {
|
|
124
144
|
const { frame } = await this._executeCommand({
|
|
125
|
-
operation:
|
|
145
|
+
operation: "hist",
|
|
126
146
|
sessionId,
|
|
127
147
|
payload: {
|
|
128
148
|
lim: normalizeHistoryLimit(options.limit),
|
|
129
149
|
bc: ensureOptionalString(options.beforeCursor),
|
|
130
150
|
},
|
|
131
|
-
expectType:
|
|
151
|
+
expectType: "historyPage",
|
|
132
152
|
});
|
|
133
153
|
return frame.history;
|
|
134
154
|
}
|
|
135
155
|
|
|
136
156
|
async sendMessage(sessionId, input = {}) {
|
|
137
157
|
const text = ensureOptionalString(input.text);
|
|
138
|
-
const attachmentIds = ensureArrayOfStrings(
|
|
158
|
+
const attachmentIds = ensureArrayOfStrings(
|
|
159
|
+
input.attachmentIds,
|
|
160
|
+
"attachmentIds",
|
|
161
|
+
);
|
|
162
|
+
const mode = ensureOptionalString(input.mode);
|
|
139
163
|
if (!text && attachmentIds.length === 0) {
|
|
140
|
-
throw new CodeKanbanValidationError(
|
|
164
|
+
throw new CodeKanbanValidationError("text or attachmentIds is required");
|
|
141
165
|
}
|
|
142
166
|
return await this._executeCommand({
|
|
143
|
-
operation:
|
|
167
|
+
operation: "send",
|
|
144
168
|
sessionId,
|
|
145
169
|
payload: {
|
|
146
170
|
txt: text,
|
|
147
171
|
atts: attachmentIds,
|
|
172
|
+
...(mode ? { mode } : {}),
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async removePendingInput(sessionId, input = {}) {
|
|
178
|
+
return await this._executeCommand({
|
|
179
|
+
operation: "pending_del",
|
|
180
|
+
sessionId,
|
|
181
|
+
payload: {
|
|
182
|
+
id: ensureString(input.pendingId, "pendingId"),
|
|
148
183
|
},
|
|
149
184
|
});
|
|
150
185
|
}
|
|
151
186
|
|
|
152
187
|
async abort(sessionId) {
|
|
153
|
-
return await this._executeCommand({ operation:
|
|
188
|
+
return await this._executeCommand({ operation: "abort", sessionId });
|
|
154
189
|
}
|
|
155
190
|
|
|
156
191
|
async approve(sessionId) {
|
|
157
|
-
return await this._executeCommand({ operation:
|
|
192
|
+
return await this._executeCommand({ operation: "approve", sessionId });
|
|
158
193
|
}
|
|
159
194
|
|
|
160
195
|
async reject(sessionId) {
|
|
161
|
-
return await this._executeCommand({ operation:
|
|
196
|
+
return await this._executeCommand({ operation: "reject", sessionId });
|
|
162
197
|
}
|
|
163
198
|
|
|
164
199
|
async answerUserInput(sessionId, input = {}) {
|
|
165
200
|
return await this._executeCommand({
|
|
166
|
-
operation:
|
|
201
|
+
operation: "user_input",
|
|
167
202
|
sessionId,
|
|
168
203
|
payload: {
|
|
169
|
-
iid: ensureString(input.itemId,
|
|
170
|
-
ans:
|
|
204
|
+
iid: ensureString(input.itemId, "itemId"),
|
|
205
|
+
ans:
|
|
206
|
+
input.answers && typeof input.answers === "object"
|
|
207
|
+
? input.answers
|
|
208
|
+
: {},
|
|
171
209
|
},
|
|
172
210
|
});
|
|
173
211
|
}
|
|
174
212
|
|
|
175
213
|
async rename(sessionId, input = {}) {
|
|
176
214
|
return await this._executeCommand({
|
|
177
|
-
operation:
|
|
215
|
+
operation: "rename",
|
|
178
216
|
sessionId,
|
|
179
217
|
payload: {
|
|
180
|
-
ttl: ensureString(input.title,
|
|
218
|
+
ttl: ensureString(input.title, "title"),
|
|
181
219
|
},
|
|
182
220
|
});
|
|
183
221
|
}
|
|
184
222
|
|
|
185
223
|
async updateModel(sessionId, input = {}) {
|
|
186
224
|
return await this._executeCommand({
|
|
187
|
-
operation:
|
|
225
|
+
operation: "set_md",
|
|
188
226
|
sessionId,
|
|
189
227
|
payload: {
|
|
190
|
-
md: ensureString(input.model,
|
|
228
|
+
md: ensureString(input.model, "model"),
|
|
191
229
|
},
|
|
192
230
|
});
|
|
193
231
|
}
|
|
194
232
|
|
|
195
233
|
async updateReasoningEffort(sessionId, input = {}) {
|
|
196
234
|
return await this._executeCommand({
|
|
197
|
-
operation:
|
|
235
|
+
operation: "set_re",
|
|
198
236
|
sessionId,
|
|
199
237
|
payload: {
|
|
200
|
-
re: ensureString(input.reasoningEffort,
|
|
238
|
+
re: ensureString(input.reasoningEffort, "reasoningEffort"),
|
|
201
239
|
},
|
|
202
240
|
});
|
|
203
241
|
}
|
|
204
242
|
|
|
205
243
|
async updateWorkflowMode(sessionId, input = {}) {
|
|
206
244
|
return await this._executeCommand({
|
|
207
|
-
operation:
|
|
245
|
+
operation: "set_wm",
|
|
208
246
|
sessionId,
|
|
209
247
|
payload: {
|
|
210
|
-
wm: ensureString(input.workflowMode,
|
|
248
|
+
wm: ensureString(input.workflowMode, "workflowMode"),
|
|
211
249
|
},
|
|
212
250
|
});
|
|
213
251
|
}
|
|
214
252
|
|
|
215
253
|
async updatePermissionLevel(sessionId, input = {}) {
|
|
216
254
|
return await this._executeCommand({
|
|
217
|
-
operation:
|
|
255
|
+
operation: "set_pl",
|
|
218
256
|
sessionId,
|
|
219
257
|
payload: {
|
|
220
|
-
pl: ensureString(input.permissionLevel,
|
|
258
|
+
pl: ensureString(input.permissionLevel, "permissionLevel"),
|
|
221
259
|
},
|
|
222
260
|
});
|
|
223
261
|
}
|
|
224
262
|
|
|
225
263
|
async updateAgent(sessionId, input = {}) {
|
|
226
264
|
return await this._executeCommand({
|
|
227
|
-
operation:
|
|
265
|
+
operation: "set_ag",
|
|
228
266
|
sessionId,
|
|
229
267
|
payload: {
|
|
230
|
-
ag: ensureString(input.agent,
|
|
268
|
+
ag: ensureString(input.agent, "agent"),
|
|
231
269
|
},
|
|
232
270
|
});
|
|
233
271
|
}
|
|
234
272
|
|
|
235
273
|
async move(sessionId, input = {}) {
|
|
236
274
|
return await this._executeCommand({
|
|
237
|
-
operation:
|
|
275
|
+
operation: "move",
|
|
238
276
|
sessionId,
|
|
239
277
|
payload: {
|
|
240
278
|
prv: ensureOptionalString(input.prevSessionId),
|
|
@@ -244,7 +282,7 @@ export class WebSessionCommandChannel {
|
|
|
244
282
|
}
|
|
245
283
|
|
|
246
284
|
async delete(sessionId) {
|
|
247
|
-
return await this._executeCommand({ operation:
|
|
285
|
+
return await this._executeCommand({ operation: "del", sessionId });
|
|
248
286
|
}
|
|
249
287
|
|
|
250
288
|
close() {
|
|
@@ -258,11 +296,16 @@ export class WebSessionCommandChannel {
|
|
|
258
296
|
return next;
|
|
259
297
|
}
|
|
260
298
|
|
|
261
|
-
async _executeCommand({
|
|
299
|
+
async _executeCommand({
|
|
300
|
+
operation,
|
|
301
|
+
sessionId,
|
|
302
|
+
payload = {},
|
|
303
|
+
expectType = null,
|
|
304
|
+
}) {
|
|
262
305
|
return await this._enqueue(async () => {
|
|
263
306
|
await this.waitForOpen();
|
|
264
307
|
if (this._closed || this.socket.readyState !== SOCKET_OPEN) {
|
|
265
|
-
throw new CodeKanbanError(
|
|
308
|
+
throw new CodeKanbanError("web session command channel is not open");
|
|
266
309
|
}
|
|
267
310
|
|
|
268
311
|
const requestId = createRequestId();
|
|
@@ -310,20 +353,24 @@ export class WebSessionCommandChannel {
|
|
|
310
353
|
_handleMessage(data) {
|
|
311
354
|
const rawFrame = decodeWebSessionSocketMessage(data);
|
|
312
355
|
if (isWebSessionHeartbeatFrame(rawFrame)) {
|
|
313
|
-
if (rawFrame?.op ===
|
|
314
|
-
this.socket.send(JSON.stringify(buildWebSessionHeartbeatFrame(
|
|
356
|
+
if (rawFrame?.op === "ping" && this.socket.readyState === SOCKET_OPEN) {
|
|
357
|
+
this.socket.send(JSON.stringify(buildWebSessionHeartbeatFrame("pong")));
|
|
315
358
|
}
|
|
316
359
|
return;
|
|
317
360
|
}
|
|
318
361
|
const frame = normalizeWebSessionFrame(rawFrame);
|
|
319
362
|
|
|
320
|
-
if (
|
|
363
|
+
if (
|
|
364
|
+
frame.type === "error" &&
|
|
365
|
+
frame.requestId &&
|
|
366
|
+
this._pendingRequests.has(frame.requestId)
|
|
367
|
+
) {
|
|
321
368
|
const pending = this._pendingRequests.get(frame.requestId);
|
|
322
369
|
this._clearAckTimer(pending);
|
|
323
370
|
this._pendingRequests.delete(frame.requestId);
|
|
324
371
|
pending?.reject(
|
|
325
372
|
new CodeKanbanError(frame.message, {
|
|
326
|
-
name:
|
|
373
|
+
name: "CodeKanbanWebSessionCommandError",
|
|
327
374
|
code: frame.code,
|
|
328
375
|
retry: frame.retry,
|
|
329
376
|
frame,
|
|
@@ -332,7 +379,11 @@ export class WebSessionCommandChannel {
|
|
|
332
379
|
return;
|
|
333
380
|
}
|
|
334
381
|
|
|
335
|
-
if (
|
|
382
|
+
if (
|
|
383
|
+
frame.type === "ack" &&
|
|
384
|
+
frame.requestId &&
|
|
385
|
+
this._pendingRequests.has(frame.requestId)
|
|
386
|
+
) {
|
|
336
387
|
const pending = this._pendingRequests.get(frame.requestId);
|
|
337
388
|
if (!pending) {
|
|
338
389
|
return;
|
|
@@ -348,7 +399,8 @@ export class WebSessionCommandChannel {
|
|
|
348
399
|
if (
|
|
349
400
|
this._pendingFollowUp &&
|
|
350
401
|
frame.type === this._pendingFollowUp.expectedType &&
|
|
351
|
-
(!this._pendingFollowUp.sessionId ||
|
|
402
|
+
(!this._pendingFollowUp.sessionId ||
|
|
403
|
+
frame.sessionId === this._pendingFollowUp.sessionId)
|
|
352
404
|
) {
|
|
353
405
|
const pendingFollowUp = this._pendingFollowUp;
|
|
354
406
|
this._pendingFollowUp = null;
|
|
@@ -364,7 +416,7 @@ export class WebSessionCommandChannel {
|
|
|
364
416
|
}
|
|
365
417
|
|
|
366
418
|
_rejectAll(error) {
|
|
367
|
-
this._pendingRequests.forEach(pending => {
|
|
419
|
+
this._pendingRequests.forEach((pending) => {
|
|
368
420
|
this._clearAckTimer(pending);
|
|
369
421
|
pending.reject(error);
|
|
370
422
|
});
|