codekanban 0.28.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,35 +1,39 @@
1
- import path from 'node:path';
1
+ import path from "node:path";
2
2
 
3
- import { CodeKanbanValidationError } from './errors.js';
4
- import { ensureArrayOfStrings, ensureOptionalString, ensureString } from './utils.js';
3
+ import { CodeKanbanValidationError } from "./errors.js";
4
+ import {
5
+ ensureArrayOfStrings,
6
+ ensureOptionalString,
7
+ ensureString,
8
+ } from "./utils.js";
5
9
 
6
10
  export const WEB_SESSION_PROTOCOL_VERSION = 1;
7
- export const WEB_SESSION_COMMAND_WS_PATH = '/api/v1/web-sessions/ws';
8
- export const WEB_SESSION_EVENTS_WS_PATH = '/api/v1/web-sessions/events';
9
- export const WEB_SESSION_HEARTBEAT_KIND = 'hb';
11
+ export const WEB_SESSION_COMMAND_WS_PATH = "/api/v1/web-sessions/ws";
12
+ export const WEB_SESSION_EVENTS_WS_PATH = "/api/v1/web-sessions/events";
13
+ export const WEB_SESSION_HEARTBEAT_KIND = "hb";
10
14
 
11
15
  const IMAGE_MIME_BY_EXT = new Map([
12
- ['.apng', 'image/apng'],
13
- ['.bmp', 'image/bmp'],
14
- ['.gif', 'image/gif'],
15
- ['.heic', 'image/heic'],
16
- ['.heif', 'image/heif'],
17
- ['.jpeg', 'image/jpeg'],
18
- ['.jpg', 'image/jpeg'],
19
- ['.png', 'image/png'],
20
- ['.svg', 'image/svg+xml'],
21
- ['.svgz', 'image/svg+xml'],
22
- ['.tif', 'image/tiff'],
23
- ['.tiff', 'image/tiff'],
24
- ['.webp', 'image/webp'],
16
+ [".apng", "image/apng"],
17
+ [".bmp", "image/bmp"],
18
+ [".gif", "image/gif"],
19
+ [".heic", "image/heic"],
20
+ [".heif", "image/heif"],
21
+ [".jpeg", "image/jpeg"],
22
+ [".jpg", "image/jpeg"],
23
+ [".png", "image/png"],
24
+ [".svg", "image/svg+xml"],
25
+ [".svgz", "image/svg+xml"],
26
+ [".tif", "image/tiff"],
27
+ [".tiff", "image/tiff"],
28
+ [".webp", "image/webp"],
25
29
  ]);
26
30
 
27
31
  function stringValue(value) {
28
- if (typeof value === 'string') {
32
+ if (typeof value === "string") {
29
33
  return value;
30
34
  }
31
35
  if (value == null) {
32
- return '';
36
+ return "";
33
37
  }
34
38
  return String(value);
35
39
  }
@@ -95,7 +99,7 @@ function normalizeHistoryToolCommandGroup(value) {
95
99
  }
96
100
 
97
101
  function normalizeHistoryTool(value) {
98
- if (!value || typeof value !== 'object') {
102
+ if (!value || typeof value !== "object") {
99
103
  return null;
100
104
  }
101
105
  return {
@@ -104,8 +108,8 @@ function normalizeHistoryTool(value) {
104
108
  kind: trimmedString(value.kind) || null,
105
109
  input: value.in,
106
110
  output: trimmedString(value.out) || null,
107
- status: trimmedString(value.st) || 'running',
108
- meta: value.meta && typeof value.meta === 'object' ? value.meta : null,
111
+ status: trimmedString(value.st) || "running",
112
+ meta: value.meta && typeof value.meta === "object" ? value.meta : null,
109
113
  commandGroup: normalizeHistoryToolCommandGroup(value.cg),
110
114
  };
111
115
  }
@@ -124,7 +128,9 @@ function normalizeUserInputQuestion(value) {
124
128
  question: trimmedString(value?.question),
125
129
  isOther: booleanValue(value?.isOther),
126
130
  isSecret: booleanValue(value?.isSecret),
127
- options: Array.isArray(value?.options) ? value.options.map(normalizeUserInputOption) : [],
131
+ options: Array.isArray(value?.options)
132
+ ? value.options.map(normalizeUserInputOption)
133
+ : [],
128
134
  };
129
135
  }
130
136
 
@@ -132,49 +138,83 @@ function normalizeHistoryAnswerEntry(value) {
132
138
  return {
133
139
  id: trimmedString(value?.id),
134
140
  label: trimmedString(value?.label),
135
- values: ensureArrayOfStrings(value?.values, 'values'),
141
+ values: ensureArrayOfStrings(value?.values, "values"),
136
142
  masked: booleanValue(value?.masked),
137
143
  };
138
144
  }
139
145
 
140
146
  function normalizeHistoryDetail(value) {
141
- if (!value || typeof value !== 'object') {
147
+ if (!value || typeof value !== "object") {
142
148
  return null;
143
149
  }
144
150
  return {
145
151
  type: trimmedString(value.type),
146
152
  prompt: trimmedString(value.prompt) || null,
147
- questions: Array.isArray(value.questions) ? value.questions.map(normalizeUserInputQuestion) : [],
148
- answers: Array.isArray(value.answers) ? value.answers.map(normalizeHistoryAnswerEntry) : [],
153
+ questions: Array.isArray(value.questions)
154
+ ? value.questions.map(normalizeUserInputQuestion)
155
+ : [],
156
+ answers: Array.isArray(value.answers)
157
+ ? value.answers.map(normalizeHistoryAnswerEntry)
158
+ : [],
149
159
  action: trimmedString(value.action) || null,
150
160
  };
151
161
  }
152
162
 
163
+ function normalizePendingInput(value) {
164
+ const id = trimmedString(value?.id);
165
+ if (!id) {
166
+ return null;
167
+ }
168
+ const mode = trimmedString(value?.m || value?.mode);
169
+ if (mode !== "redirect" && mode !== "queue") {
170
+ return null;
171
+ }
172
+ return {
173
+ id,
174
+ mode,
175
+ text: stringValue(value?.txt ?? value?.text),
176
+ attachmentIds: ensureArrayOfStrings(
177
+ value?.atts ?? value?.attachmentIds,
178
+ "attachmentIds",
179
+ ),
180
+ createdAt:
181
+ isoFromUnixMilli(value?.ca) ||
182
+ (typeof value?.createdAt === "string"
183
+ ? trimmedString(value.createdAt) || null
184
+ : null),
185
+ };
186
+ }
187
+
153
188
  export function normalizeWebSessionHistoryItem(value) {
154
- const updatedTimestamp = isoFromUnixMilli(value?.ts2) || isoFromUnixMilli(value?.obs);
189
+ const updatedTimestamp =
190
+ isoFromUnixMilli(value?.ts2) || isoFromUnixMilli(value?.obs);
155
191
  return {
156
192
  id: trimmedString(value?.id),
157
193
  sourceTurnId: trimmedString(value?.stid) || null,
158
194
  sourceItemId: trimmedString(value?.siid) || null,
159
195
  orderIndex: numberValue(value?.oi, 0),
160
- kind: trimmedString(value?.kd) || 'system',
196
+ kind: trimmedString(value?.kd) || "system",
161
197
  itemType: trimmedString(value?.tp),
162
198
  text: stringValue(value?.txt),
163
199
  timestamp: isoFromUnixMilli(value?.ts2),
164
200
  observedAt: isoFromUnixMilli(value?.obs),
165
- attachments: Array.isArray(value?.atts) ? value.atts.map(normalizeHistoryAttachment) : [],
201
+ attachments: Array.isArray(value?.atts)
202
+ ? value.atts.map(normalizeHistoryAttachment)
203
+ : [],
166
204
  tool: normalizeHistoryTool(value?.tl),
167
205
  level: trimmedString(value?.lvl) || null,
168
206
  done: booleanValue(value?.dn),
169
207
  detail: normalizeHistoryDetail(value?.dt),
170
- payload: value?.pl && typeof value.pl === 'object' ? value.pl : null,
208
+ payload: value?.pl && typeof value.pl === "object" ? value.pl : null,
171
209
  updatedAt: updatedTimestamp,
172
210
  };
173
211
  }
174
212
 
175
213
  export function normalizeWebSessionHistoryWindow(value) {
176
214
  return {
177
- items: Array.isArray(value?.its) ? value.its.map(normalizeWebSessionHistoryItem) : [],
215
+ items: Array.isArray(value?.its)
216
+ ? value.its.map(normalizeWebSessionHistoryItem)
217
+ : [],
178
218
  hasMore: booleanValue(value?.hm),
179
219
  beforeCursor: trimmedString(value?.bc) || null,
180
220
  total: numberValue(value?.tot, 0),
@@ -188,23 +228,24 @@ export function normalizeWebSessionSummaryFromWire(value) {
188
228
  projectId: trimmedString(value?.pid),
189
229
  worktreeId: trimmedString(value?.wid) || null,
190
230
  orderIndex: numberValue(value?.oi, 0),
191
- agent: trimmedString(value?.ag) || 'codex',
231
+ agent: trimmedString(value?.ag) || "codex",
192
232
  title: trimmedString(value?.ttl),
193
233
  model: trimmedString(value?.md),
194
- reasoningEffort: trimmedString(value?.re) || 'default',
195
- workflowMode: trimmedString(value?.wm) || 'default',
196
- permissionLevel: trimmedString(value?.pl) || 'elevated',
234
+ reasoningEffort: trimmedString(value?.re) || "default",
235
+ workflowMode: trimmedString(value?.wm) || "default",
236
+ permissionLevel: trimmedString(value?.pl) || "elevated",
197
237
  cwd: trimmedString(value?.cwd),
198
238
  nativeSessionId: trimmedString(value?.nsid) || null,
199
- status: trimmedString(value?.st) || 'idle',
239
+ status: trimmedString(value?.st) || "idle",
200
240
  assistantState: trimmedString(value?.ast) || null,
201
241
  hasUnread: booleanValue(value?.unr),
202
242
  archivedAt: isoFromUnixMilli(value?.aa),
203
- activityAt: isoFromUnixMilli(value?.act) || new Date(updatedAtMs).toISOString(),
243
+ activityAt:
244
+ isoFromUnixMilli(value?.act) || new Date(updatedAtMs).toISOString(),
204
245
  lastMessageAt: isoFromUnixMilli(value?.lma),
205
246
  assistantStateUpdatedAt: isoFromUnixMilli(value?.asu),
206
- sourceKind: trimmedString(value?.sk) || 'codex_app_server',
207
- syncState: trimmedString(value?.ss) || 'missing',
247
+ sourceKind: trimmedString(value?.sk) || "codex_app_server",
248
+ syncState: trimmedString(value?.ss) || "missing",
208
249
  lastSyncMode: trimmedString(value?.lsm) || null,
209
250
  sourceCreatedAt: isoFromUnixMilli(value?.sca),
210
251
  sourceUpdatedAt: isoFromUnixMilli(value?.sua),
@@ -214,14 +255,15 @@ export function normalizeWebSessionSummaryFromWire(value) {
214
255
  turnCount: numberValue(value?.tc, 0),
215
256
  itemCount: numberValue(value?.ic, 0),
216
257
  syncError: trimmedString(value?.se) || null,
217
- createdAt: isoFromUnixMilli(value?.ca) || new Date(updatedAtMs).toISOString(),
258
+ createdAt:
259
+ isoFromUnixMilli(value?.ca) || new Date(updatedAtMs).toISOString(),
218
260
  updatedAt: new Date(updatedAtMs).toISOString(),
219
261
  usage: {
220
262
  ...normalizeUsage(value?.usa),
221
263
  cost: numberValue(value?.cost, 0),
222
264
  },
223
265
  contextWindowTokens: nullableNumberValue(value?.cwt),
224
- contextWindowSource: trimmedString(value?.cws) || 'unavailable',
266
+ contextWindowSource: trimmedString(value?.cws) || "unavailable",
225
267
  };
226
268
  }
227
269
 
@@ -229,11 +271,15 @@ export function normalizeWebSessionSnapshotFromWire(frame) {
229
271
  return {
230
272
  session: normalizeWebSessionSummaryFromWire(frame?.s),
231
273
  history: normalizeWebSessionHistoryWindow(frame?.h),
274
+ pendingInputs: Array.isArray(frame?.pi)
275
+ ? frame.pi.map(normalizePendingInput).filter(Boolean)
276
+ : [],
232
277
  };
233
278
  }
234
279
 
235
- const PROCESS_RESTART_REASON = 'process_restart';
236
- const DEFAULT_RECOVERY_MESSAGE = 'Session runtime was interrupted. Send a new message to continue.';
280
+ const PROCESS_RESTART_REASON = "process_restart";
281
+ const DEFAULT_RECOVERY_MESSAGE =
282
+ "Session runtime was interrupted. Send a new message to continue.";
237
283
 
238
284
  function isProcessRestartPayload(payload) {
239
285
  return trimmedString(payload?.reason) === PROCESS_RESTART_REASON;
@@ -244,26 +290,35 @@ function getRecoveryMessage(payload) {
244
290
  }
245
291
 
246
292
  function normalizeChoiceText(value) {
247
- return trimmedString(value)
248
- .toLowerCase()
249
- .replace(/\s+/g, ' ');
293
+ return trimmedString(value).toLowerCase().replace(/\s+/g, " ");
250
294
  }
251
295
 
252
296
  function isExecutePlanOption(option) {
253
- const text = normalizeChoiceText(`${option?.label || ''} ${option?.description || ''}`);
297
+ const text = normalizeChoiceText(
298
+ `${option?.label || ""} ${option?.description || ""}`,
299
+ );
254
300
  const mentionsPlan = /计划|plan/.test(text);
255
- const mentionsExecute = /开始|执行|实现|实施|继续|start|execute|implement|proceed/.test(text);
301
+ const mentionsExecute =
302
+ /开始|执行|实现|实施|继续|start|execute|implement|proceed/.test(text);
256
303
  const mentionsCancel = /取消|暂不|稍后|later|cancel|dismiss|hold/.test(text);
257
304
  return mentionsExecute && (mentionsPlan || !mentionsCancel);
258
305
  }
259
306
 
260
307
  function isCancelPlanOption(option) {
261
- const text = normalizeChoiceText(`${option?.label || ''} ${option?.description || ''}`);
262
- return /取消|暂不|稍后|later|cancel|dismiss|hold|keep planning|stay in plan/.test(text);
308
+ const text = normalizeChoiceText(
309
+ `${option?.label || ""} ${option?.description || ""}`,
310
+ );
311
+ return /取消|暂不|稍后|later|cancel|dismiss|hold|keep planning|stay in plan/.test(
312
+ text,
313
+ );
263
314
  }
264
315
 
265
316
  function isPlanChoiceQuestion(question) {
266
- if (!question || !Array.isArray(question.options) || question.options.length !== 2) {
317
+ if (
318
+ !question ||
319
+ !Array.isArray(question.options) ||
320
+ question.options.length !== 2
321
+ ) {
267
322
  return false;
268
323
  }
269
324
  const hasExecute = question.options.some(isExecutePlanOption);
@@ -274,21 +329,25 @@ function isPlanChoiceQuestion(question) {
274
329
  function findPendingApproval(items) {
275
330
  let pending = null;
276
331
  for (const item of items) {
277
- if (item?.detail?.type === 'approval_request') {
332
+ if (item?.detail?.type === "approval_request") {
278
333
  pending = {
279
334
  id: item.id,
280
335
  itemId: item.sourceItemId || item.id,
281
- prompt: item.detail.prompt || item.text || '',
336
+ prompt: item.detail.prompt || item.text || "",
282
337
  requestedAt: item.timestamp || item.observedAt || null,
283
338
  stale: false,
284
339
  };
285
340
  continue;
286
341
  }
287
- if (item?.detail?.type === 'approval_response' || item?.kind === 'user') {
342
+ if (item?.detail?.type === "approval_response" || item?.kind === "user") {
288
343
  pending = null;
289
344
  continue;
290
345
  }
291
- if (item?.itemType === 'run_abort' && pending && isProcessRestartPayload(item.payload || undefined)) {
346
+ if (
347
+ item?.itemType === "run_abort" &&
348
+ pending &&
349
+ isProcessRestartPayload(item.payload || undefined)
350
+ ) {
292
351
  pending = {
293
352
  ...pending,
294
353
  stale: true,
@@ -297,7 +356,7 @@ function findPendingApproval(items) {
297
356
  };
298
357
  continue;
299
358
  }
300
- if (item?.itemType === 'run_abort' || item?.itemType === 'run_fail') {
359
+ if (item?.itemType === "run_abort" || item?.itemType === "run_fail") {
301
360
  pending = null;
302
361
  }
303
362
  }
@@ -307,8 +366,10 @@ function findPendingApproval(items) {
307
366
  function findPendingUserInput(items) {
308
367
  let pending = null;
309
368
  for (const item of items) {
310
- if (item?.detail?.type === 'user_input_request') {
311
- const questions = Array.isArray(item.detail.questions) ? item.detail.questions : [];
369
+ if (item?.detail?.type === "user_input_request") {
370
+ const questions = Array.isArray(item.detail.questions)
371
+ ? item.detail.questions
372
+ : [];
312
373
  const question = questions[0];
313
374
  const executeOption =
314
375
  questions.length === 1 && isPlanChoiceQuestion(question)
@@ -316,22 +377,29 @@ function findPendingUserInput(items) {
316
377
  : null;
317
378
  pending = {
318
379
  id: item.id,
319
- itemId: item.sourceItemId || trimmedString(item.payload?.iid) || item.id,
320
- prompt: item.detail.prompt || item.text || '',
380
+ itemId:
381
+ item.sourceItemId || trimmedString(item.payload?.iid) || item.id,
382
+ prompt: item.detail.prompt || item.text || "",
321
383
  questions,
322
384
  requestedAt: item.timestamp || item.observedAt || null,
323
385
  stale: false,
324
386
  isPlanChoice: Boolean(executeOption),
325
387
  questionId: executeOption ? trimmedString(question?.id) || null : null,
326
- executeOptionLabel: executeOption ? trimmedString(executeOption.label) || null : null,
388
+ executeOptionLabel: executeOption
389
+ ? trimmedString(executeOption.label) || null
390
+ : null,
327
391
  };
328
392
  continue;
329
393
  }
330
- if (item?.detail?.type === 'user_input_response' || item?.kind === 'user') {
394
+ if (item?.detail?.type === "user_input_response" || item?.kind === "user") {
331
395
  pending = null;
332
396
  continue;
333
397
  }
334
- if (item?.itemType === 'run_abort' && pending && isProcessRestartPayload(item.payload || undefined)) {
398
+ if (
399
+ item?.itemType === "run_abort" &&
400
+ pending &&
401
+ isProcessRestartPayload(item.payload || undefined)
402
+ ) {
335
403
  pending = {
336
404
  ...pending,
337
405
  stale: true,
@@ -340,7 +408,7 @@ function findPendingUserInput(items) {
340
408
  };
341
409
  continue;
342
410
  }
343
- if (item?.itemType === 'run_abort' || item?.itemType === 'run_fail') {
411
+ if (item?.itemType === "run_abort" || item?.itemType === "run_fail") {
344
412
  pending = null;
345
413
  }
346
414
  }
@@ -350,18 +418,20 @@ function findPendingUserInput(items) {
350
418
  function findLatestPlan(items) {
351
419
  const latestPlan = [...items]
352
420
  .reverse()
353
- .find(item => item?.kind === 'tool' && item?.tool?.kind === 'plan');
421
+ .find((item) => item?.kind === "tool" && item?.tool?.kind === "plan");
354
422
  if (!latestPlan) {
355
423
  return null;
356
424
  }
357
425
  const hasUserMessageAfter = items.some(
358
- item => item?.kind === 'user' && numberValue(item.orderIndex, 0) > numberValue(latestPlan.orderIndex, 0),
426
+ (item) =>
427
+ item?.kind === "user" &&
428
+ numberValue(item.orderIndex, 0) > numberValue(latestPlan.orderIndex, 0),
359
429
  );
360
430
  return {
361
431
  id: latestPlan.id,
362
432
  itemId: latestPlan.id,
363
433
  toolId: latestPlan.tool.id || latestPlan.id,
364
- output: latestPlan.tool.output || latestPlan.text || '',
434
+ output: latestPlan.tool.output || latestPlan.text || "",
365
435
  timestamp: latestPlan.timestamp || latestPlan.observedAt || null,
366
436
  orderIndex: latestPlan.orderIndex,
367
437
  hasUserMessageAfter,
@@ -370,14 +440,16 @@ function findLatestPlan(items) {
370
440
  }
371
441
 
372
442
  function findLastAssistantMessage(items) {
373
- const lastAssistant = [...items].reverse().find(item => item?.kind === 'assistant');
443
+ const lastAssistant = [...items]
444
+ .reverse()
445
+ .find((item) => item?.kind === "assistant");
374
446
  if (!lastAssistant) {
375
447
  return null;
376
448
  }
377
449
  return {
378
450
  id: lastAssistant.id,
379
451
  itemId: lastAssistant.id,
380
- text: lastAssistant.text || '',
452
+ text: lastAssistant.text || "",
381
453
  timestamp: lastAssistant.timestamp || lastAssistant.observedAt || null,
382
454
  orderIndex: lastAssistant.orderIndex,
383
455
  };
@@ -385,40 +457,48 @@ function findLastAssistantMessage(items) {
385
457
 
386
458
  export function analyzeWebSession(snapshot) {
387
459
  const session = snapshot?.session || null;
388
- const history = snapshot?.history || { items: [], hasMore: false, beforeCursor: null, total: 0 };
460
+ const history = snapshot?.history || {
461
+ items: [],
462
+ hasMore: false,
463
+ beforeCursor: null,
464
+ total: 0,
465
+ };
466
+ const pendingInputs = Array.isArray(snapshot?.pendingInputs)
467
+ ? snapshot.pendingInputs.filter(Boolean)
468
+ : [];
389
469
  const items = Array.isArray(history.items) ? history.items : [];
390
470
  const pendingApproval = findPendingApproval(items);
391
471
  const pendingUserInput = findPendingUserInput(items);
392
472
  const latestPlan = findLatestPlan(items);
393
473
  const lastAssistantMessage = findLastAssistantMessage(items);
394
474
 
395
- let phase = 'idle';
396
- if (session?.assistantState === 'waiting_input') {
397
- phase = 'waiting_input';
398
- } else if (session?.assistantState === 'waiting_approval') {
399
- phase = 'waiting_approval';
400
- } else if (session?.assistantState === 'waiting_plan_approval') {
401
- phase = 'waiting_plan_approval';
402
- } else if (session?.status === 'running') {
403
- phase = 'running';
404
- } else if (session?.status === 'done') {
405
- phase = 'done';
406
- } else if (session?.status === 'err') {
407
- phase = 'error';
408
- } else if (session?.status === 'aborting') {
409
- phase = 'aborting';
475
+ let phase = "idle";
476
+ if (session?.assistantState === "waiting_input") {
477
+ phase = "waiting_input";
478
+ } else if (session?.assistantState === "waiting_approval") {
479
+ phase = "waiting_approval";
480
+ } else if (session?.assistantState === "waiting_plan_approval") {
481
+ phase = "waiting_plan_approval";
482
+ } else if (session?.status === "running") {
483
+ phase = "running";
484
+ } else if (session?.status === "done") {
485
+ phase = "done";
486
+ } else if (session?.status === "err") {
487
+ phase = "error";
488
+ } else if (session?.status === "aborting") {
489
+ phase = "aborting";
410
490
  }
411
491
 
412
492
  let nextAction = null;
413
493
  if (pendingApproval) {
414
494
  nextAction = {
415
- type: 'approval',
495
+ type: "approval",
416
496
  prompt: pendingApproval.prompt,
417
497
  requestedAt: pendingApproval.requestedAt,
418
498
  };
419
499
  } else if (pendingUserInput?.isPlanChoice && latestPlan?.awaitingExecution) {
420
500
  nextAction = {
421
- type: 'execute_plan',
501
+ type: "execute_plan",
422
502
  itemId: pendingUserInput.itemId,
423
503
  questionId: pendingUserInput.questionId,
424
504
  executeOptionLabel: pendingUserInput.executeOptionLabel,
@@ -426,24 +506,27 @@ export function analyzeWebSession(snapshot) {
426
506
  };
427
507
  } else if (pendingUserInput) {
428
508
  nextAction = {
429
- type: 'answer_user_input',
509
+ type: "answer_user_input",
430
510
  itemId: pendingUserInput.itemId,
431
511
  prompt: pendingUserInput.prompt,
432
512
  questions: pendingUserInput.questions,
433
513
  requestedAt: pendingUserInput.requestedAt,
434
514
  };
435
- } else if (phase === 'waiting_plan_approval' && latestPlan?.awaitingExecution) {
515
+ } else if (
516
+ phase === "waiting_plan_approval" &&
517
+ latestPlan?.awaitingExecution
518
+ ) {
436
519
  nextAction = {
437
- type: 'execute_plan',
520
+ type: "execute_plan",
438
521
  latestPlan,
439
522
  };
440
523
  }
441
524
 
442
525
  const canSend =
443
- phase === 'idle' ||
444
- phase === 'done' ||
445
- phase === 'error' ||
446
- phase === 'waiting_plan_approval';
526
+ phase === "idle" ||
527
+ phase === "done" ||
528
+ phase === "error" ||
529
+ phase === "waiting_plan_approval";
447
530
 
448
531
  return {
449
532
  phase,
@@ -452,27 +535,31 @@ export function analyzeWebSession(snapshot) {
452
535
  nextAction,
453
536
  pendingApproval,
454
537
  pendingUserInput,
538
+ pendingInputs,
455
539
  latestPlan,
456
540
  lastAssistantMessage,
457
541
  session,
458
542
  snapshot: {
459
543
  session,
460
544
  history,
545
+ pendingInputs,
461
546
  },
462
547
  };
463
548
  }
464
549
 
465
550
  export function decodeWebSessionSocketMessage(raw) {
466
- if (typeof raw === 'string') {
551
+ if (typeof raw === "string") {
467
552
  return JSON.parse(raw);
468
553
  }
469
554
  if (raw instanceof ArrayBuffer) {
470
- return JSON.parse(Buffer.from(raw).toString('utf8'));
555
+ return JSON.parse(Buffer.from(raw).toString("utf8"));
471
556
  }
472
557
  if (ArrayBuffer.isView(raw)) {
473
- return JSON.parse(Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength).toString('utf8'));
558
+ return JSON.parse(
559
+ Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength).toString("utf8"),
560
+ );
474
561
  }
475
- return JSON.parse(String(raw ?? ''));
562
+ return JSON.parse(String(raw ?? ""));
476
563
  }
477
564
 
478
565
  export function normalizeWebSessionFrame(rawFrame) {
@@ -486,21 +573,21 @@ export function normalizeWebSessionFrame(rawFrame) {
486
573
  raw: rawFrame,
487
574
  };
488
575
 
489
- if (rawFrame?.k === 'ack') {
576
+ if (rawFrame?.k === "ack") {
490
577
  return {
491
578
  ...base,
492
- type: 'ack',
579
+ type: "ack",
493
580
  ok: rawFrame?.ok === 1 || rawFrame?.ok === true,
494
581
  payload: rawFrame?.p ?? null,
495
582
  };
496
583
  }
497
584
 
498
- if (rawFrame?.k === 'err') {
585
+ if (rawFrame?.k === "err") {
499
586
  return {
500
587
  ...base,
501
- type: 'error',
502
- code: trimmedString(rawFrame?.code) || 'unknown_error',
503
- message: trimmedString(rawFrame?.msg) || 'Unknown websocket error',
588
+ type: "error",
589
+ code: trimmedString(rawFrame?.code) || "unknown_error",
590
+ message: trimmedString(rawFrame?.msg) || "Unknown websocket error",
504
591
  retry: booleanValue(rawFrame?.retry),
505
592
  };
506
593
  }
@@ -508,63 +595,79 @@ export function normalizeWebSessionFrame(rawFrame) {
508
595
  if (rawFrame?.k === WEB_SESSION_HEARTBEAT_KIND) {
509
596
  return {
510
597
  ...base,
511
- type: 'heartbeat',
598
+ type: "heartbeat",
512
599
  heartbeatType: trimmedString(rawFrame?.op) || null,
513
600
  };
514
601
  }
515
602
 
516
- if (rawFrame?.k === 'snap') {
603
+ if (rawFrame?.k === "snap") {
517
604
  return {
518
605
  ...base,
519
- type: 'snapshot',
606
+ type: "snapshot",
520
607
  snapshot: normalizeWebSessionSnapshotFromWire(rawFrame),
521
608
  };
522
609
  }
523
610
 
524
- if (rawFrame?.k === 'evt') {
525
- if (rawFrame?.op === 'session' && rawFrame?.s) {
611
+ if (rawFrame?.k === "evt") {
612
+ if (rawFrame?.op === "session" && rawFrame?.s) {
526
613
  return {
527
614
  ...base,
528
- type: 'session',
615
+ type: "session",
529
616
  session: normalizeWebSessionSummaryFromWire(rawFrame.s),
530
617
  };
531
618
  }
532
- if (rawFrame?.op === 'hist_page' && rawFrame?.h) {
619
+ if (rawFrame?.op === "hist_page" && rawFrame?.h) {
533
620
  return {
534
621
  ...base,
535
- type: 'historyPage',
622
+ type: "historyPage",
536
623
  history: normalizeWebSessionHistoryWindow(rawFrame.h),
537
624
  };
538
625
  }
539
- if (rawFrame?.op === 'hist_item' && rawFrame?.i) {
626
+ if (rawFrame?.op === "hist_item" && rawFrame?.i) {
540
627
  return {
541
628
  ...base,
542
- type: 'historyItem',
629
+ type: "historyItem",
543
630
  item: normalizeWebSessionHistoryItem(rawFrame.i),
544
- session: rawFrame?.s ? normalizeWebSessionSummaryFromWire(rawFrame.s) : null,
631
+ session: rawFrame?.s
632
+ ? normalizeWebSessionSummaryFromWire(rawFrame.s)
633
+ : null,
634
+ };
635
+ }
636
+ if (rawFrame?.op === "pending") {
637
+ return {
638
+ ...base,
639
+ type: "pending",
640
+ pendingInputs: Array.isArray(rawFrame?.pi)
641
+ ? rawFrame.pi.map(normalizePendingInput).filter(Boolean)
642
+ : [],
545
643
  };
546
644
  }
547
645
  return {
548
646
  ...base,
549
- type: 'event',
647
+ type: "event",
550
648
  payload: rawFrame?.p ?? null,
551
649
  };
552
650
  }
553
651
 
554
652
  return {
555
653
  ...base,
556
- type: 'unknown',
654
+ type: "unknown",
557
655
  payload: rawFrame?.p ?? null,
558
656
  };
559
657
  }
560
658
 
561
- export function buildWebSessionCommandFrame({ requestId, sessionId, operation, payload }) {
659
+ export function buildWebSessionCommandFrame({
660
+ requestId,
661
+ sessionId,
662
+ operation,
663
+ payload,
664
+ }) {
562
665
  return {
563
666
  v: WEB_SESSION_PROTOCOL_VERSION,
564
- k: 'cmd',
565
- rid: ensureString(requestId, 'requestId'),
667
+ k: "cmd",
668
+ rid: ensureString(requestId, "requestId"),
566
669
  sid: ensureOptionalString(sessionId) || undefined,
567
- op: ensureString(operation, 'operation'),
670
+ op: ensureString(operation, "operation"),
568
671
  p: payload ?? {},
569
672
  };
570
673
  }
@@ -574,7 +677,7 @@ export function buildWebSessionHeartbeatFrame(operation) {
574
677
  v: WEB_SESSION_PROTOCOL_VERSION,
575
678
  k: WEB_SESSION_HEARTBEAT_KIND,
576
679
  ts: Date.now(),
577
- op: ensureString(operation, 'operation'),
680
+ op: ensureString(operation, "operation"),
578
681
  };
579
682
  }
580
683
 
@@ -583,7 +686,7 @@ export function isWebSessionHeartbeatFrame(rawFrame) {
583
686
  }
584
687
 
585
688
  export function normalizeWebSessionAttachment(value) {
586
- if (!value || typeof value !== 'object') {
689
+ if (!value || typeof value !== "object") {
587
690
  return null;
588
691
  }
589
692
  return {
@@ -592,21 +695,23 @@ export function normalizeWebSessionAttachment(value) {
592
695
  mime: trimmedString(value.mime),
593
696
  size: numberValue(value.size, 0),
594
697
  path: trimmedString(value.path),
595
- createdAt: stringValue(value.createdAt || ''),
698
+ createdAt: stringValue(value.createdAt || ""),
596
699
  };
597
700
  }
598
701
 
599
702
  export function inferWebSessionAttachmentMimeType(fileName) {
600
703
  const extension = path.extname(trimmedString(fileName)).toLowerCase();
601
- return IMAGE_MIME_BY_EXT.get(extension) || '';
704
+ return IMAGE_MIME_BY_EXT.get(extension) || "";
602
705
  }
603
706
 
604
707
  export function ensureImageMimeType(value, fileName) {
605
708
  const explicit = ensureOptionalString(value);
606
709
  const inferred = inferWebSessionAttachmentMimeType(fileName);
607
710
  const mimeType = explicit || inferred;
608
- if (!mimeType || !mimeType.startsWith('image/')) {
609
- throw new CodeKanbanValidationError('web session attachments must be image files');
711
+ if (!mimeType || !mimeType.startsWith("image/")) {
712
+ throw new CodeKanbanValidationError(
713
+ "web session attachments must be image files",
714
+ );
610
715
  }
611
716
  return mimeType;
612
717
  }
@@ -614,15 +719,19 @@ export function ensureImageMimeType(value, fileName) {
614
719
  export function shouldEmitWebSessionFrame(frame, sessionIdFilter) {
615
720
  const normalizedFilter = ensureOptionalString(sessionIdFilter);
616
721
  if (!normalizedFilter) {
617
- return frame?.type !== 'heartbeat';
722
+ return frame?.type !== "heartbeat";
618
723
  }
619
- if (!frame || typeof frame !== 'object') {
724
+ if (!frame || typeof frame !== "object") {
620
725
  return false;
621
726
  }
622
- if (frame.type === 'open' || frame.type === 'close' || frame.type === 'error') {
727
+ if (
728
+ frame.type === "open" ||
729
+ frame.type === "close" ||
730
+ frame.type === "error"
731
+ ) {
623
732
  return true;
624
733
  }
625
- if (frame.type === 'heartbeat') {
734
+ if (frame.type === "heartbeat") {
626
735
  return false;
627
736
  }
628
737
  return ensureOptionalString(frame.sessionId) === normalizedFilter;