@cadraos/sdk 0.3.9 → 0.3.10

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.
@@ -298,7 +298,7 @@ function useKanbanBoard(options) {
298
298
  try {
299
299
  const full = await adapter.getCard(cardId);
300
300
  setSelectedCard(
301
- (prev) => prev && prev.id === cardId ? full : prev
301
+ (prev) => prev == null || prev.id === cardId ? full : prev
302
302
  );
303
303
  } catch (err) {
304
304
  if (!known) fail(err, "Failed to open card");
@@ -407,6 +407,27 @@ function useKanbanBoard(options) {
407
407
  },
408
408
  [refresh]
409
409
  );
410
+ const deleteCard = useCallback(
411
+ async (cardId) => {
412
+ if (!adapter.deleteCard) return false;
413
+ setError(null);
414
+ const prevCards = cards;
415
+ const prevSelected = selectedCard;
416
+ setCards((prev) => prev.filter((c) => c.id !== cardId));
417
+ setSelectedCard((prev) => prev && prev.id === cardId ? null : prev);
418
+ try {
419
+ await adapter.deleteCard(cardId);
420
+ void silentRefresh();
421
+ return true;
422
+ } catch (err) {
423
+ setCards(prevCards);
424
+ setSelectedCard(prevSelected);
425
+ fail(err, "Failed to delete card");
426
+ return false;
427
+ }
428
+ },
429
+ [adapter, cards, selectedCard, silentRefresh, fail]
430
+ );
410
431
  return {
411
432
  board,
412
433
  lanes: board?.lanes ?? [],
@@ -422,6 +443,7 @@ function useKanbanBoard(options) {
422
443
  createCard,
423
444
  updateCard,
424
445
  transitionCard,
446
+ deleteCard,
425
447
  handleRunTerminal
426
448
  };
427
449
  }
@@ -2715,6 +2737,9 @@ function KanbanCard({
2715
2737
  onClick,
2716
2738
  selected,
2717
2739
  dueState,
2740
+ draggable,
2741
+ onDragStart,
2742
+ onDragEnd,
2718
2743
  className
2719
2744
  }) {
2720
2745
  const runStatus = card.run?.status;
@@ -2743,10 +2768,20 @@ function KanbanCard({
2743
2768
  onClick: () => onClick?.(card),
2744
2769
  "aria-pressed": selected,
2745
2770
  "aria-busy": isWorking,
2771
+ draggable,
2772
+ onDragStart: draggable ? (e) => {
2773
+ e.dataTransfer.effectAllowed = "move";
2774
+ e.dataTransfer.setData("text/plain", card.id);
2775
+ onDragStart?.(card);
2776
+ } : void 0,
2777
+ onDragEnd: draggable ? () => onDragEnd?.() : void 0,
2746
2778
  className: cn9(
2747
- "relative w-full text-left rounded-lg border bg-card p-3 shadow-sm transition",
2779
+ // Solid card: fill + border + a faint token top-sheen (adapts to theme),
2780
+ // matching the host's cockpit cards (work-shell v2 visual pass).
2781
+ "relative w-full text-left rounded-xl border bg-card bg-gradient-to-b from-foreground/[0.03] to-transparent p-3 shadow-sm motion-safe:transition-shadow",
2748
2782
  "hover:shadow-md hover:border-primary/40",
2749
2783
  "focus:outline-none focus:ring-2 focus:ring-ring/50",
2784
+ draggable && "cursor-grab active:cursor-grabbing",
2750
2785
  // The working card carries the SINGLE focal motion on the board (F11).
2751
2786
  isWorking ? "border-info/60 ring-2 ring-info/40 animate-pulse" : selected ? "border-primary/60 ring-2 ring-primary/30" : "border-border",
2752
2787
  className
@@ -2823,6 +2858,11 @@ function CapMeter({ count, cap, className }) {
2823
2858
  }
2824
2859
  );
2825
2860
  }
2861
+ var AGENT_GROUPS = [
2862
+ { kind: "cadra", label: "Cadra agents" },
2863
+ { kind: "remote", label: "Your own agents" },
2864
+ { kind: "human", label: "Teammates" }
2865
+ ];
2826
2866
  function AgentKindIcon({ kind }) {
2827
2867
  const common = {
2828
2868
  viewBox: "0 0 24 24",
@@ -2865,17 +2905,55 @@ function OnlineDot({ online }) {
2865
2905
  }
2866
2906
  );
2867
2907
  }
2908
+ function RemoteBadge() {
2909
+ return /* @__PURE__ */ jsx("span", { className: "shrink-0 rounded-full border border-accent/40 bg-accent/15 px-1.5 py-px text-[10px] font-semibold leading-4 text-accent", children: "Remote" });
2910
+ }
2868
2911
  function LaneAgentPicker({
2869
2912
  selected,
2870
2913
  options,
2871
2914
  onSelect,
2872
2915
  canEdit = true,
2873
2916
  lockedReason,
2917
+ onEditAgent,
2874
2918
  className
2875
2919
  }) {
2876
2920
  const [open, setOpen] = useState(false);
2921
+ const [placement, setPlacement] = useState(null);
2877
2922
  const rootRef = useRef(null);
2923
+ const triggerRef = useRef(null);
2878
2924
  const editable = canEdit && !!onSelect && (options?.length ?? 0) > 0;
2925
+ const openMenu = () => {
2926
+ const el = triggerRef.current;
2927
+ if (el && typeof window !== "undefined") {
2928
+ const rect = el.getBoundingClientRect();
2929
+ const vw = window.innerWidth || 0;
2930
+ const vh = window.innerHeight || 0;
2931
+ const margin = 8;
2932
+ const gap = 4;
2933
+ const desired = 288;
2934
+ const width = Math.max(
2935
+ Math.min(rect.width || 224, Math.max(vw - margin * 2, 160)),
2936
+ 160
2937
+ );
2938
+ const left = Math.min(
2939
+ Math.max(rect.left, margin),
2940
+ Math.max(vw - width - margin, margin)
2941
+ );
2942
+ const spaceBelow = vh - rect.bottom - gap - margin;
2943
+ const spaceAbove = rect.top - gap - margin;
2944
+ const flipUp = spaceBelow < desired && spaceAbove > spaceBelow;
2945
+ const maxHeight = Math.max(
2946
+ Math.min(desired, flipUp ? spaceAbove : spaceBelow),
2947
+ 120
2948
+ );
2949
+ setPlacement(
2950
+ flipUp ? { bottom: Math.max(vh - rect.top + gap, margin), left, width, maxHeight } : { top: rect.bottom + gap, left, width, maxHeight }
2951
+ );
2952
+ } else {
2953
+ setPlacement(null);
2954
+ }
2955
+ setOpen(true);
2956
+ };
2879
2957
  useEffect(() => {
2880
2958
  if (!open) return;
2881
2959
  const onDocDown = (e) => {
@@ -2909,14 +2987,28 @@ function LaneAgentPicker({
2909
2987
  }
2910
2988
  ),
2911
2989
  /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: displayLabel }),
2990
+ kind === "remote" && /* @__PURE__ */ jsx(RemoteBadge, {}),
2912
2991
  /* @__PURE__ */ jsx(OnlineDot, { online: selected?.online })
2913
2992
  ] });
2993
+ const grouped = AGENT_GROUPS.map((group) => ({
2994
+ ...group,
2995
+ items: (options ?? []).filter((opt) => (opt.kind ?? "cadra") === group.kind)
2996
+ })).filter((group) => group.items.length > 0);
2997
+ const menuStyle = placement ? {
2998
+ position: "fixed",
2999
+ top: placement.top,
3000
+ bottom: placement.bottom,
3001
+ left: placement.left,
3002
+ width: placement.width,
3003
+ maxHeight: placement.maxHeight
3004
+ } : void 0;
2914
3005
  return /* @__PURE__ */ jsxs("div", { ref: rootRef, className: cn9("relative mt-2 w-full", className), children: [
2915
3006
  editable ? /* @__PURE__ */ jsxs(
2916
3007
  "button",
2917
3008
  {
3009
+ ref: triggerRef,
2918
3010
  type: "button",
2919
- onClick: () => setOpen((o) => !o),
3011
+ onClick: () => open ? setOpen(false) : openMenu(),
2920
3012
  "aria-haspopup": "listbox",
2921
3013
  "aria-expanded": open,
2922
3014
  "aria-label": `Assigned agent: ${displayLabel}. Change`,
@@ -2994,47 +3086,106 @@ function LaneAgentPicker({
2994
3086
  ),
2995
3087
  /* @__PURE__ */ jsx("span", { children: lockedReason })
2996
3088
  ] }),
2997
- editable && open && /* @__PURE__ */ jsx(
2998
- "ul",
3089
+ editable && open && /* @__PURE__ */ jsxs(
3090
+ "div",
2999
3091
  {
3000
- role: "listbox",
3001
- "aria-label": "Assign an agent",
3092
+ style: menuStyle,
3002
3093
  className: cn9(
3003
- "absolute z-20 mt-1 max-h-64 w-full overflow-y-auto rounded-lg border border-border",
3004
- "bg-popover p-1 shadow-lg"
3094
+ "flex flex-col overflow-hidden rounded-lg border border-border bg-popover shadow-lg",
3095
+ menuStyle ? "z-50" : "absolute z-20 mt-1 max-h-64 w-full"
3005
3096
  ),
3006
- children: options.map((opt) => {
3007
- const isSelected = opt.id === selected?.id;
3008
- return /* @__PURE__ */ jsx("li", { role: "option", "aria-selected": isSelected, children: /* @__PURE__ */ jsxs(
3097
+ children: [
3098
+ /* @__PURE__ */ jsx(
3099
+ "ul",
3100
+ {
3101
+ role: "listbox",
3102
+ "aria-label": "Assign an agent",
3103
+ className: "min-h-0 flex-1 overflow-y-auto p-1",
3104
+ children: grouped.map((group) => /* @__PURE__ */ jsxs("li", { role: "presentation", children: [
3105
+ /* @__PURE__ */ jsx("div", { className: "px-2 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground", children: group.label }),
3106
+ /* @__PURE__ */ jsx("ul", { role: "group", "aria-label": group.label, children: group.items.map((opt) => {
3107
+ const isSelected = opt.id === selected?.id;
3108
+ const optKind = opt.kind ?? "cadra";
3109
+ return /* @__PURE__ */ jsx("li", { role: "option", "aria-selected": isSelected, children: /* @__PURE__ */ jsxs(
3110
+ "button",
3111
+ {
3112
+ type: "button",
3113
+ onClick: () => {
3114
+ onSelect(opt.id);
3115
+ setOpen(false);
3116
+ },
3117
+ className: cn9(
3118
+ "flex min-h-11 w-full items-center gap-1.5 rounded-md px-2 text-left text-xs font-medium",
3119
+ "hover:bg-muted focus:outline-none focus:ring-2 focus:ring-ring/50",
3120
+ isSelected ? "text-primary" : "text-foreground"
3121
+ ),
3122
+ children: [
3123
+ /* @__PURE__ */ jsx(
3124
+ "span",
3125
+ {
3126
+ className: cn9(
3127
+ "grid h-5 w-5 shrink-0 place-items-center rounded-full",
3128
+ optKind === "cadra" ? "bg-primary/15 text-primary" : "bg-secondary text-foreground"
3129
+ ),
3130
+ children: /* @__PURE__ */ jsx(AgentKindIcon, { kind: optKind })
3131
+ }
3132
+ ),
3133
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: opt.label }),
3134
+ optKind === "remote" && /* @__PURE__ */ jsx(RemoteBadge, {}),
3135
+ /* @__PURE__ */ jsx(OnlineDot, { online: opt.online }),
3136
+ isSelected && /* @__PURE__ */ jsx(
3137
+ "svg",
3138
+ {
3139
+ viewBox: "0 0 24 24",
3140
+ fill: "none",
3141
+ stroke: "currentColor",
3142
+ strokeWidth: 2,
3143
+ strokeLinecap: "round",
3144
+ strokeLinejoin: "round",
3145
+ "aria-hidden": true,
3146
+ className: "h-3.5 w-3.5 shrink-0",
3147
+ children: /* @__PURE__ */ jsx("path", { d: "M20 6L9 17l-5-5" })
3148
+ }
3149
+ )
3150
+ ]
3151
+ }
3152
+ ) }, opt.id);
3153
+ }) })
3154
+ ] }, group.kind))
3155
+ }
3156
+ ),
3157
+ onEditAgent && selected && /* @__PURE__ */ jsx("div", { className: "border-t border-border p-1", children: /* @__PURE__ */ jsxs(
3009
3158
  "button",
3010
3159
  {
3011
3160
  type: "button",
3012
3161
  onClick: () => {
3013
- onSelect(opt.id);
3162
+ onEditAgent(selected.id);
3014
3163
  setOpen(false);
3015
3164
  },
3016
3165
  className: cn9(
3017
- "flex min-h-11 w-full items-center gap-1.5 rounded-md px-2 text-left text-xs font-medium",
3018
- "hover:bg-muted focus:outline-none focus:ring-2 focus:ring-ring/50",
3019
- isSelected ? "text-primary" : "text-foreground"
3166
+ "flex min-h-11 w-full items-center gap-1.5 rounded-md px-2 text-left text-xs font-medium text-foreground",
3167
+ "hover:bg-muted focus:outline-none focus:ring-2 focus:ring-ring/50"
3020
3168
  ),
3021
3169
  children: [
3022
3170
  /* @__PURE__ */ jsx(
3023
- "span",
3171
+ "svg",
3024
3172
  {
3025
- className: cn9(
3026
- "grid h-5 w-5 shrink-0 place-items-center rounded-full",
3027
- (opt.kind ?? "cadra") === "cadra" ? "bg-primary/15 text-primary" : "bg-secondary text-foreground"
3028
- ),
3029
- children: /* @__PURE__ */ jsx(AgentKindIcon, { kind: opt.kind ?? "cadra" })
3173
+ viewBox: "0 0 24 24",
3174
+ fill: "none",
3175
+ stroke: "currentColor",
3176
+ strokeWidth: 2,
3177
+ strokeLinecap: "round",
3178
+ strokeLinejoin: "round",
3179
+ "aria-hidden": true,
3180
+ className: "h-3.5 w-3.5 shrink-0 text-muted-foreground",
3181
+ children: /* @__PURE__ */ jsx("path", { d: "M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" })
3030
3182
  }
3031
3183
  ),
3032
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: opt.label }),
3033
- /* @__PURE__ */ jsx(OnlineDot, { online: opt.online })
3184
+ "Edit this agent"
3034
3185
  ]
3035
3186
  }
3036
- ) }, opt.id);
3037
- })
3187
+ ) })
3188
+ ]
3038
3189
  }
3039
3190
  )
3040
3191
  ] });
@@ -3059,6 +3210,12 @@ function KanbanColumn({
3059
3210
  count,
3060
3211
  agentPicker,
3061
3212
  canEditHandlers,
3213
+ draggableCards,
3214
+ dragActive,
3215
+ dropAllowed,
3216
+ onCardDragStart,
3217
+ onCardDragEnd,
3218
+ onLaneDrop,
3062
3219
  className
3063
3220
  }) {
3064
3221
  const headerCount = count ?? cards.length;
@@ -3069,9 +3226,21 @@ function KanbanColumn({
3069
3226
  return /* @__PURE__ */ jsxs(
3070
3227
  "div",
3071
3228
  {
3229
+ onDragOver: dropAllowed ? (e) => {
3230
+ e.preventDefault();
3231
+ e.dataTransfer.dropEffect = "move";
3232
+ } : void 0,
3233
+ onDrop: dropAllowed && onLaneDrop ? (e) => {
3234
+ e.preventDefault();
3235
+ onLaneDrop(lane);
3236
+ } : void 0,
3072
3237
  className: cn9(
3073
- "flex w-[85vw] max-w-sm snap-start shrink-0 flex-col rounded-xl bg-muted/40 border border-border sm:w-72 sm:max-w-none",
3238
+ "flex w-[85vw] max-w-sm snap-start shrink-0 flex-col rounded-xl bg-muted/40 border border-border transition sm:w-72 sm:max-w-none",
3074
3239
  isScheduled && "border-border",
3240
+ // MV4 drag affordance: allowed targets glow; other lanes dim so the
3241
+ // valid drop zones read at a glance.
3242
+ dragActive && dropAllowed && "border-primary/60 ring-2 ring-primary/40 bg-primary/5",
3243
+ dragActive && !dropAllowed && "opacity-55",
3075
3244
  className
3076
3245
  ),
3077
3246
  children: [
@@ -3140,7 +3309,10 @@ function KanbanColumn({
3140
3309
  card,
3141
3310
  onClick: onCardClick,
3142
3311
  selected: card.id === selectedCardId,
3143
- dueState: dueStateFor(card, lane, now)
3312
+ dueState: dueStateFor(card, lane, now),
3313
+ draggable: draggableCards,
3314
+ onDragStart: onCardDragStart,
3315
+ onDragEnd: onCardDragEnd
3144
3316
  },
3145
3317
  card.id
3146
3318
  )),
@@ -3300,6 +3472,8 @@ function KanbanBoard({
3300
3472
  laneAgentPicker,
3301
3473
  scheduleStatus,
3302
3474
  focusCardId,
3475
+ renderCardDrawer,
3476
+ enableDrag,
3303
3477
  className
3304
3478
  }) {
3305
3479
  const {
@@ -3314,6 +3488,7 @@ function KanbanBoard({
3314
3488
  createCard,
3315
3489
  updateCard,
3316
3490
  transitionCard,
3491
+ deleteCard,
3317
3492
  handleRunTerminal
3318
3493
  } = useKanbanBoard({ adapter, onError, onCardCreated, pollIntervalMs });
3319
3494
  const lastFocusedRef = useRef(void 0);
@@ -3358,6 +3533,44 @@ function KanbanBoard({
3358
3533
  (cardId, toLane) => transitionCard({ cardId, targetLane: toLane }),
3359
3534
  [transitionCard]
3360
3535
  );
3536
+ const [dragging, setDragging] = useState(null);
3537
+ const draggingRef = useRef(null);
3538
+ const dropInFlightRef = useRef(null);
3539
+ const allowedDropLanes = useMemo(() => {
3540
+ if (!dragging || !board?.transitions) return /* @__PURE__ */ new Set();
3541
+ return new Set(
3542
+ board.transitions.filter((t) => t.fromLane === dragging.lane && t.manualAllowed).map((t) => t.toLane)
3543
+ );
3544
+ }, [dragging, board?.transitions]);
3545
+ const handleCardDragStart = useCallback((card) => {
3546
+ const d = { id: card.id, lane: card.lane };
3547
+ draggingRef.current = d;
3548
+ setDragging(d);
3549
+ }, []);
3550
+ const handleCardDragEnd = useCallback(() => {
3551
+ draggingRef.current = null;
3552
+ setDragging(null);
3553
+ }, []);
3554
+ const handleLaneDrop = useCallback(
3555
+ (lane) => {
3556
+ const cur = draggingRef.current;
3557
+ draggingRef.current = null;
3558
+ setDragging(null);
3559
+ if (!cur || lane.key === cur.lane) return;
3560
+ if (dropInFlightRef.current === cur.id) return;
3561
+ const allowed = (board?.transitions ?? []).some(
3562
+ (t) => t.fromLane === cur.lane && t.toLane === lane.key && t.manualAllowed
3563
+ );
3564
+ if (!allowed) return;
3565
+ dropInFlightRef.current = cur.id;
3566
+ void Promise.resolve(transitionCard({ cardId: cur.id, targetLane: lane.key })).finally(
3567
+ () => {
3568
+ if (dropInFlightRef.current === cur.id) dropInFlightRef.current = null;
3569
+ }
3570
+ );
3571
+ },
3572
+ [board?.transitions, transitionCard]
3573
+ );
3361
3574
  const initialLaneKey = lanes.find((l) => !l.terminal)?.key ?? lanes[0]?.key;
3362
3575
  const selectedLane = selectedCard ? lanes.find((l) => l.key === selectedCard.lane) : void 0;
3363
3576
  const canAdvanceSelected = !!selectedLane && !selectedLane.terminal;
@@ -3378,12 +3591,32 @@ function KanbanBoard({
3378
3591
  showCreate: enableCreate && lane.key === initialLaneKey,
3379
3592
  count: laneCounts?.[lane.key],
3380
3593
  agentPicker: laneAgentPicker?.(lane),
3381
- canEditHandlers
3594
+ canEditHandlers,
3595
+ draggableCards: enableDrag && enableAdvance,
3596
+ dragActive: !!dragging,
3597
+ dropAllowed: !!dragging && dragging.lane !== lane.key && allowedDropLanes.has(lane.key),
3598
+ onCardDragStart: handleCardDragStart,
3599
+ onCardDragEnd: handleCardDragEnd,
3600
+ onLaneDrop: handleLaneDrop
3382
3601
  },
3383
3602
  lane.id ?? lane.key
3384
3603
  ))
3385
3604
  ] }),
3386
- selectedCard && /* @__PURE__ */ jsx(
3605
+ selectedCard && (renderCardDrawer ? renderCardDrawer({
3606
+ card: selectedCard,
3607
+ adapter,
3608
+ lanes,
3609
+ transitions: enableAdvance ? board?.transitions : void 0,
3610
+ canAdvance: canAdvanceSelected,
3611
+ onClose: closeCard,
3612
+ onRetry: onRetryCard ? handleRetry : void 0,
3613
+ onUpdate: enableEdit ? handleUpdate : void 0,
3614
+ onAdvance: enableAdvance ? handleAdvance : void 0,
3615
+ onTransition: enableAdvance ? handleTransition : void 0,
3616
+ onDelete: adapter.deleteCard ? deleteCard : void 0,
3617
+ onRunTerminal: handleRunTerminal,
3618
+ onError
3619
+ }) : /* @__PURE__ */ jsx(
3387
3620
  CardDrawer,
3388
3621
  {
3389
3622
  card: selectedCard,
@@ -3399,7 +3632,7 @@ function KanbanBoard({
3399
3632
  onRunTerminal: handleRunTerminal,
3400
3633
  onError
3401
3634
  }
3402
- )
3635
+ ))
3403
3636
  ] })
3404
3637
  ] });
3405
3638
  }
@@ -3424,6 +3657,7 @@ function composeAdapter(source, boardConfigUuid, version, cardSource) {
3424
3657
  transitionCard(input) {
3425
3658
  return cardSource.transitionCard(input);
3426
3659
  },
3660
+ ...cardSource.deleteCard ? { deleteCard: (id) => cardSource.deleteCard(id) } : {},
3427
3661
  ...cardSource.createRunStream ? { createRunStream: (id) => cardSource.createRunStream(id) } : {}
3428
3662
  };
3429
3663
  }
@@ -4797,7 +5031,6 @@ var AgentsAPI = class extends BaseResourceAPI {
4797
5031
  super(http, "/api/v1/agents");
4798
5032
  this.getStreamConfig = getStreamConfig;
4799
5033
  }
4800
- getStreamConfig;
4801
5034
  // ===== LIST Operations =====
4802
5035
  /**
4803
5036
  * List all agents in the organization
@@ -5703,7 +5936,6 @@ var SessionsAPI = class extends BaseResourceAPI {
5703
5936
  super(http, "/api/v1/sessions");
5704
5937
  this.getStreamConfig = getStreamConfig;
5705
5938
  }
5706
- getStreamConfig;
5707
5939
  // ===== LIST Operations =====
5708
5940
  /**
5709
5941
  * List all sessions in the organization
@@ -6023,7 +6255,6 @@ var KanbanAPI = class extends BaseResourceAPI {
6023
6255
  super(http, "/api/v1/kanban");
6024
6256
  this.getStreamConfig = getStreamConfig;
6025
6257
  }
6026
- getStreamConfig;
6027
6258
  // ===== BOARD =====
6028
6259
  /**
6029
6260
  * Get the current org's board (lanes in declared order). Auto-provisions
@@ -6225,7 +6456,6 @@ var RemoteAgentsAPI = class extends BaseResourceAPI {
6225
6456
  super(http, "/api/v1/remote-agents");
6226
6457
  this.getStreamConfig = getStreamConfig;
6227
6458
  }
6228
- getStreamConfig;
6229
6459
  /**
6230
6460
  * List all remote agents in the organization
6231
6461
  *