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 { CodeKanbanConfigError, CodeKanbanError, CodeKanbanValidationError } from './errors.js';
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 './web-session-shared.js';
10
- import { ensureArrayOfStrings, ensureOptionalString, ensureString } from './utils.js';
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, 'url');
38
+ const resolvedUrl = ensureString(url, "url");
31
39
  const Socket = WebSocketImpl || globalThis.WebSocket;
32
40
  if (!Socket) {
33
- throw new CodeKanbanConfigError('WebSocket implementation is unavailable');
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('open', () => {
58
+ this.socket.addEventListener("open", () => {
49
59
  this._resolveOpen?.();
50
60
  });
51
61
 
52
- this.socket.addEventListener('message', event => {
62
+ this.socket.addEventListener("message", (event) => {
53
63
  this._handleMessage(event.data);
54
64
  });
55
65
 
56
- this.socket.addEventListener('error', event => {
57
- const error = new CodeKanbanError('web session command websocket error', { event });
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('close', event => {
74
+ this.socket.addEventListener("close", (event) => {
63
75
  this._closed = true;
64
- const error = new CodeKanbanError('web session command websocket closed', { event });
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
- () => reject(new CodeKanbanValidationError(`web session command channel did not open within ${timeoutMs}ms`)),
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: 'list',
107
+ operation: "list",
88
108
  payload: {
89
- pid: ensureString(projectId, '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: 'create',
117
+ operation: "create",
98
118
  payload: {
99
- pid: ensureString(input.projectId, 'projectId'),
119
+ pid: ensureString(input.projectId, "projectId"),
100
120
  wid: ensureOptionalString(input.worktreeId),
101
- ag: ensureString(input.agent, '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: 'snapshot',
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: 'connect',
136
+ operation: "connect",
117
137
  sessionId,
118
- expectType: 'snapshot',
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: 'hist',
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: 'historyPage',
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(input.attachmentIds, 'attachmentIds');
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('text or attachmentIds is required');
164
+ throw new CodeKanbanValidationError("text or attachmentIds is required");
141
165
  }
142
166
  return await this._executeCommand({
143
- operation: 'send',
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: 'abort', sessionId });
188
+ return await this._executeCommand({ operation: "abort", sessionId });
154
189
  }
155
190
 
156
191
  async approve(sessionId) {
157
- return await this._executeCommand({ operation: 'approve', sessionId });
192
+ return await this._executeCommand({ operation: "approve", sessionId });
158
193
  }
159
194
 
160
195
  async reject(sessionId) {
161
- return await this._executeCommand({ operation: 'reject', sessionId });
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: 'user_input',
201
+ operation: "user_input",
167
202
  sessionId,
168
203
  payload: {
169
- iid: ensureString(input.itemId, 'itemId'),
170
- ans: input.answers && typeof input.answers === 'object' ? input.answers : {},
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: 'rename',
215
+ operation: "rename",
178
216
  sessionId,
179
217
  payload: {
180
- ttl: ensureString(input.title, '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: 'set_md',
225
+ operation: "set_md",
188
226
  sessionId,
189
227
  payload: {
190
- md: ensureString(input.model, '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: 'set_re',
235
+ operation: "set_re",
198
236
  sessionId,
199
237
  payload: {
200
- re: ensureString(input.reasoningEffort, '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: 'set_wm',
245
+ operation: "set_wm",
208
246
  sessionId,
209
247
  payload: {
210
- wm: ensureString(input.workflowMode, '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: 'set_pl',
255
+ operation: "set_pl",
218
256
  sessionId,
219
257
  payload: {
220
- pl: ensureString(input.permissionLevel, '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: 'set_ag',
265
+ operation: "set_ag",
228
266
  sessionId,
229
267
  payload: {
230
- ag: ensureString(input.agent, '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: 'move',
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: 'del', sessionId });
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({ operation, sessionId, payload = {}, expectType = null }) {
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('web session command channel is not open');
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 === 'ping' && this.socket.readyState === SOCKET_OPEN) {
314
- this.socket.send(JSON.stringify(buildWebSessionHeartbeatFrame('pong')));
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 (frame.type === 'error' && frame.requestId && this._pendingRequests.has(frame.requestId)) {
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: 'CodeKanbanWebSessionCommandError',
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 (frame.type === 'ack' && frame.requestId && this._pendingRequests.has(frame.requestId)) {
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 || frame.sessionId === 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
  });