@junctionpanel/server 0.1.97 → 0.1.99

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.
@@ -216,8 +216,14 @@ const REWIND_COMMAND = {
216
216
  description: "Rewind tracked files to a previous user message",
217
217
  argumentHint: "[user_message_uuid]",
218
218
  };
219
- const INTERRUPT_TOOL_USE_PLACEHOLDER = "[Request interrupted by user for tool use]";
219
+ const INTERRUPT_PLACEHOLDERS = new Set([
220
+ "[Request interrupted by user]",
221
+ "[Request interrupted by user for tool use]",
222
+ ]);
220
223
  const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
224
+ function isInterruptPlaceholderText(text) {
225
+ return INTERRUPT_PLACEHOLDERS.has(text.trim());
226
+ }
221
227
  function resolveClaudeBinary() {
222
228
  const claudePath = resolveCommandPathWithFallback("claude", {
223
229
  env: process.env,
@@ -387,10 +393,30 @@ function coerceToolResultContentToString(content) {
387
393
  }
388
394
  return deterministicStringify(content);
389
395
  }
396
+ const CLAUDE_LOCAL_COMMAND_ENVELOPE_PATTERN = /^<local-command-(stdout|stderr)>\s*([\s\S]*?)\s*<\/local-command-\1>$/i;
397
+ const CLAUDE_MODEL_SWITCH_ACK_PATTERN = /^set model to\b/i;
398
+ function unwrapClaudeLocalCommandEnvelope(content) {
399
+ const match = CLAUDE_LOCAL_COMMAND_ENVELOPE_PATTERN.exec(content.trim());
400
+ if (!match) {
401
+ return null;
402
+ }
403
+ const innerText = match[2]?.trim() ?? "";
404
+ return innerText.length > 0 ? innerText : null;
405
+ }
406
+ function isHiddenClaudeLocalCommandText(content) {
407
+ const innerText = unwrapClaudeLocalCommandEnvelope(content);
408
+ if (!innerText) {
409
+ return false;
410
+ }
411
+ return CLAUDE_MODEL_SWITCH_ACK_PATTERN.test(innerText);
412
+ }
390
413
  export function extractUserMessageText(content) {
391
414
  if (typeof content === "string") {
392
415
  const normalized = content.trim();
393
- return normalized.length > 0 ? normalized : null;
416
+ if (normalized.length === 0 || isHiddenClaudeLocalCommandText(normalized)) {
417
+ return null;
418
+ }
419
+ return normalized;
394
420
  }
395
421
  if (!Array.isArray(content)) {
396
422
  return null;
@@ -414,7 +440,10 @@ export function extractUserMessageText(content) {
414
440
  return null;
415
441
  }
416
442
  const combined = parts.join("\n\n").trim();
417
- return combined.length > 0 ? combined : null;
443
+ if (combined.length === 0 || isHiddenClaudeLocalCommandText(combined)) {
444
+ return null;
445
+ }
446
+ return combined;
418
447
  }
419
448
  const MAX_SUB_AGENT_LOG_ENTRIES = 200;
420
449
  const MAX_SUB_AGENT_SUMMARY_CHARS = 160;
@@ -597,14 +626,15 @@ function resolveClaudePermissionDecision(input) {
597
626
  return "prompt";
598
627
  }
599
628
  if (input.kind === "plan") {
600
- return input.lastNonPlanMode === "bypassPermissions"
601
- ? "allow_plan_transition"
602
- : "prompt";
629
+ return "prompt";
603
630
  }
604
631
  if (input.kind !== "tool") {
605
632
  return "prompt";
606
633
  }
607
- const effectiveMode = input.currentMode === "plan" ? input.lastNonPlanMode : input.currentMode;
634
+ const effectiveMode = input.currentMode === "plan" &&
635
+ input.lastNonPlanMode === "bypassPermissions"
636
+ ? "bypassPermissions"
637
+ : input.currentMode;
608
638
  if (effectiveMode === "bypassPermissions") {
609
639
  return "allow_silent";
610
640
  }
@@ -909,8 +939,7 @@ class TimelineAssembler {
909
939
  emitNewContent(state) {
910
940
  const items = [];
911
941
  const nextAssistantText = state.assistantText.slice(state.emittedAssistantLength);
912
- if (nextAssistantText.length > 0 &&
913
- nextAssistantText !== INTERRUPT_TOOL_USE_PLACEHOLDER) {
942
+ if (nextAssistantText.length > 0 && !isInterruptPlaceholderText(nextAssistantText)) {
914
943
  state.emittedAssistantLength = state.assistantText.length;
915
944
  items.push({ type: "assistant_message", text: nextAssistantText });
916
945
  }
@@ -1226,26 +1255,6 @@ class ClaudeAgentSession {
1226
1255
  updatedInput: input,
1227
1256
  };
1228
1257
  }
1229
- if (decision === "allow_plan_transition") {
1230
- this.currentMode = "bypassPermissions";
1231
- this.cachedRuntimeInfo = null;
1232
- this.pushToolCall(mapClaudeCompletedToolCall({
1233
- name: "plan_approval",
1234
- callId: options.toolUseID ?? requestId,
1235
- input,
1236
- output: { approved: true, automatic: true },
1237
- }));
1238
- this.pushEvent({
1239
- type: "permission_resolved",
1240
- provider: "claude",
1241
- requestId,
1242
- resolution: { behavior: "allow" },
1243
- });
1244
- return {
1245
- behavior: "allow",
1246
- updatedInput: input,
1247
- };
1248
- }
1249
1258
  const metadata = {};
1250
1259
  if (options.toolUseID) {
1251
1260
  metadata.toolUseId = options.toolUseID;
@@ -2026,8 +2035,7 @@ class ClaudeAgentSession {
2026
2035
  cwd: this.config.cwd,
2027
2036
  includePartialMessages: true,
2028
2037
  permissionMode: this.currentMode,
2029
- ...(this.currentMode === "bypassPermissions" ||
2030
- this.lastNonPlanMode === "bypassPermissions"
2038
+ ...(this.currentMode === "bypassPermissions"
2031
2039
  ? { allowDangerouslySkipPermissions: true }
2032
2040
  : {}),
2033
2041
  agents: this.defaults?.agents,
@@ -2472,7 +2480,16 @@ class ClaudeAgentSession {
2472
2480
  }
2473
2481
  const activeRuns = this.runTracker.listActiveRuns();
2474
2482
  if (activeRuns.length > 0) {
2483
+ const treatDoneAsInterrupted = this.pendingInterruptPromise !== null;
2475
2484
  for (const run of activeRuns) {
2485
+ if (treatDoneAsInterrupted) {
2486
+ this.cancelRun(run, {
2487
+ type: "turn_canceled",
2488
+ provider: "claude",
2489
+ reason: "Interrupted",
2490
+ });
2491
+ continue;
2492
+ }
2476
2493
  this.failRun(run, "Claude stream ended before terminal result");
2477
2494
  }
2478
2495
  }
@@ -2964,12 +2981,15 @@ class ClaudeAgentSession {
2964
2981
  break;
2965
2982
  }
2966
2983
  if (typeof content === "string" && content.length > 0) {
2967
- // String content from user messages (e.g., local command output)
2984
+ const normalizedText = extractUserMessageText(content);
2985
+ if (!normalizedText) {
2986
+ break;
2987
+ }
2968
2988
  events.push({
2969
2989
  type: "timeline",
2970
2990
  item: {
2971
2991
  type: "user_message",
2972
- text: content,
2992
+ text: normalizedText,
2973
2993
  ...(messageId ? { messageId } : {}),
2974
2994
  },
2975
2995
  provider: "claude",
@@ -3263,7 +3283,7 @@ class ClaudeAgentSession {
3263
3283
  const suppressAssistant = options?.suppressAssistantText ?? false;
3264
3284
  const suppressReasoning = options?.suppressReasoning ?? false;
3265
3285
  if (typeof content === "string") {
3266
- if (!content || content === INTERRUPT_TOOL_USE_PLACEHOLDER) {
3286
+ if (!content || isInterruptPlaceholderText(content)) {
3267
3287
  return [];
3268
3288
  }
3269
3289
  if (suppressAssistant) {
@@ -3276,7 +3296,7 @@ class ClaudeAgentSession {
3276
3296
  switch (block.type) {
3277
3297
  case "text":
3278
3298
  case "text_delta":
3279
- if (block.text && block.text !== INTERRUPT_TOOL_USE_PLACEHOLDER) {
3299
+ if (block.text && !isInterruptPlaceholderText(block.text)) {
3280
3300
  if (!suppressAssistant) {
3281
3301
  items.push({ type: "assistant_message", text: block.text });
3282
3302
  }
@@ -3718,6 +3738,74 @@ function normalizeHistoryBlocks(content) {
3718
3738
  }
3719
3739
  return null;
3720
3740
  }
3741
+ function formatProposedPlanBlock(planText) {
3742
+ return `<proposed_plan>\n${planText}\n</proposed_plan>`;
3743
+ }
3744
+ function extractToolResultToolName(block) {
3745
+ return (readTrimmedString(block.tool_name) ??
3746
+ readTrimmedString(block.name) ??
3747
+ null);
3748
+ }
3749
+ function isPlanApprovalToolResult(block) {
3750
+ const toolName = extractToolResultToolName(block)?.toLowerCase() ?? "";
3751
+ return toolName === "exitplanmode" || toolName === "plan_approval";
3752
+ }
3753
+ function extractApprovedPlanFromText(text) {
3754
+ const match = text.match(/## Approved Plan(?: \(edited by user\))?:\s*([\s\S]*)$/i);
3755
+ const planText = match?.[1]?.trim();
3756
+ return planText && planText.length > 0 ? planText : null;
3757
+ }
3758
+ function extractApprovedPlanFromPayload(value) {
3759
+ if (!isMetadata(value)) {
3760
+ return null;
3761
+ }
3762
+ return readTrimmedString(value.plan) ?? null;
3763
+ }
3764
+ function extractApprovedPlanFromHistoryBlock(block) {
3765
+ const type = readTrimmedString(block.type)?.toLowerCase() ?? "";
3766
+ if (!type.includes("tool_result")) {
3767
+ return null;
3768
+ }
3769
+ if (!isPlanApprovalToolResult(block)) {
3770
+ return null;
3771
+ }
3772
+ const planFromPayload = extractApprovedPlanFromPayload(block.toolUseResult) ??
3773
+ extractApprovedPlanFromPayload(block.tool_use_result);
3774
+ if (planFromPayload) {
3775
+ return planFromPayload;
3776
+ }
3777
+ const contentText = coerceToolResultContentToString(block.content).trim();
3778
+ if (contentText.length === 0) {
3779
+ return null;
3780
+ }
3781
+ return extractApprovedPlanFromText(contentText);
3782
+ }
3783
+ function extractApprovedPlanFromHistoryEntry(entry) {
3784
+ if (entry?.type !== "user") {
3785
+ return null;
3786
+ }
3787
+ const content = entry?.message?.content;
3788
+ const normalizedBlocks = normalizeHistoryBlocks(content);
3789
+ if (!normalizedBlocks) {
3790
+ return null;
3791
+ }
3792
+ for (const block of normalizedBlocks) {
3793
+ const planText = extractApprovedPlanFromHistoryBlock(block);
3794
+ if (planText) {
3795
+ return planText;
3796
+ }
3797
+ }
3798
+ return null;
3799
+ }
3800
+ const APPROVED_PLAN_RESOLUTION_MARKER = "Plan approved. Execute it now.";
3801
+ function buildApprovedPlanTimelineItem(planText) {
3802
+ // Append a resolved marker so replayed approved plans stay extractable as cards
3803
+ // without being mistaken for an unresolved timeline-only plan review.
3804
+ return {
3805
+ type: "assistant_message",
3806
+ text: `${formatProposedPlanBlock(planText)}\n\n${APPROVED_PLAN_RESOLUTION_MARKER}`,
3807
+ };
3808
+ }
3721
3809
  export function convertClaudeHistoryEntry(entry, mapBlocks) {
3722
3810
  if (entry.type === "system" && entry.subtype === "compact_boundary") {
3723
3811
  const compactMetadata = readCompactionMetadata(entry);
@@ -3762,6 +3850,7 @@ export function convertClaudeHistoryEntry(entry, mapBlocks) {
3762
3850
  return [taskNotificationItem];
3763
3851
  }
3764
3852
  }
3853
+ const approvedPlan = extractApprovedPlanFromHistoryEntry(entry);
3765
3854
  const timeline = [];
3766
3855
  if (entry.type === "user") {
3767
3856
  const text = extractUserMessageText(content);
@@ -3777,7 +3866,17 @@ export function convertClaudeHistoryEntry(entry, mapBlocks) {
3777
3866
  const mapped = mapBlocks(normalizedBlocks);
3778
3867
  if (entry.type === "user") {
3779
3868
  const toolItems = mapped.filter((item) => item.type === "tool_call");
3780
- return timeline.length ? [...timeline, ...toolItems] : toolItems;
3869
+ const nextTimeline = timeline.length
3870
+ ? [
3871
+ ...timeline,
3872
+ ...(approvedPlan ? [buildApprovedPlanTimelineItem(approvedPlan)] : []),
3873
+ ...toolItems,
3874
+ ]
3875
+ : [
3876
+ ...(approvedPlan ? [buildApprovedPlanTimelineItem(approvedPlan)] : []),
3877
+ ...toolItems,
3878
+ ];
3879
+ return nextTimeline;
3781
3880
  }
3782
3881
  return mapped;
3783
3882
  }
@@ -3962,22 +4061,26 @@ async function parseClaudeSessionDescriptor(filePath, mtime) {
3962
4061
  timeline,
3963
4062
  };
3964
4063
  }
3965
- function extractClaudeUserText(message) {
4064
+ export function extractClaudeUserText(message) {
3966
4065
  if (!message) {
3967
4066
  return null;
3968
4067
  }
3969
4068
  if (typeof message.content === "string") {
3970
- return message.content.trim();
4069
+ const normalized = message.content.trim();
4070
+ if (normalized.length === 0 || isHiddenClaudeLocalCommandText(normalized)) {
4071
+ return null;
4072
+ }
4073
+ return normalized;
3971
4074
  }
3972
4075
  if (typeof message.text === "string") {
3973
- return message.text.trim();
4076
+ const normalized = message.text.trim();
4077
+ if (normalized.length === 0 || isHiddenClaudeLocalCommandText(normalized)) {
4078
+ return null;
4079
+ }
4080
+ return normalized;
3974
4081
  }
3975
4082
  if (Array.isArray(message.content)) {
3976
- for (const block of message.content) {
3977
- if (block && typeof block.text === "string") {
3978
- return block.text.trim();
3979
- }
3980
- }
4083
+ return extractUserMessageText(message.content);
3981
4084
  }
3982
4085
  return null;
3983
4086
  }