@parhelia/core 0.3.12816 → 0.3.12818

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.
Files changed (48) hide show
  1. package/dist/editor/ContentTree.d.ts +1 -1
  2. package/dist/editor/ContentTree.js +25 -3
  3. package/dist/editor/ContentTree.js.map +1 -1
  4. package/dist/editor/ai/terminal/useAgentStopCommand.js +16 -14
  5. package/dist/editor/ai/terminal/useAgentStopCommand.js.map +1 -1
  6. package/dist/editor/client/operations.js +12 -3
  7. package/dist/editor/client/operations.js.map +1 -1
  8. package/dist/editor/media-selector/Thumbnails.js +23 -10
  9. package/dist/editor/media-selector/Thumbnails.js.map +1 -1
  10. package/dist/editor/media-selector/mediaItemDrag.d.ts +22 -5
  11. package/dist/editor/media-selector/mediaItemDrag.js +108 -10
  12. package/dist/editor/media-selector/mediaItemDrag.js.map +1 -1
  13. package/dist/editor/settings/panels/ProjectTemplatesPanel.js +45 -1
  14. package/dist/editor/settings/panels/ProjectTemplatesPanel.js.map +1 -1
  15. package/dist/revision.d.ts +2 -2
  16. package/dist/revision.js +2 -2
  17. package/dist/task-board/TaskBoardWorkspace.js +186 -7
  18. package/dist/task-board/TaskBoardWorkspace.js.map +1 -1
  19. package/dist/task-board/components/CreateTaskDialog.d.ts +2 -0
  20. package/dist/task-board/components/CreateTaskDialog.js +6 -3
  21. package/dist/task-board/components/CreateTaskDialog.js.map +1 -1
  22. package/dist/task-board/components/GoalCoverageBar.d.ts +4 -0
  23. package/dist/task-board/components/GoalCoverageBar.js +22 -0
  24. package/dist/task-board/components/GoalCoverageBar.js.map +1 -0
  25. package/dist/task-board/components/GoalDataView.d.ts +39 -0
  26. package/dist/task-board/components/GoalDataView.js +385 -0
  27. package/dist/task-board/components/GoalDataView.js.map +1 -0
  28. package/dist/task-board/components/GoalsMatrix.d.ts +16 -0
  29. package/dist/task-board/components/GoalsMatrix.js +129 -0
  30. package/dist/task-board/components/GoalsMatrix.js.map +1 -0
  31. package/dist/task-board/components/ProjectDashboard.d.ts +1 -1
  32. package/dist/task-board/components/ProjectGoalsPanel.d.ts +16 -0
  33. package/dist/task-board/components/ProjectGoalsPanel.js +74 -113
  34. package/dist/task-board/components/ProjectGoalsPanel.js.map +1 -1
  35. package/dist/task-board/components/SubgoalStatusIcon.d.ts +4 -0
  36. package/dist/task-board/components/SubgoalStatusIcon.js +19 -0
  37. package/dist/task-board/components/SubgoalStatusIcon.js.map +1 -0
  38. package/dist/task-board/components/goalDisplaySpec.d.ts +19 -0
  39. package/dist/task-board/components/goalDisplaySpec.js +72 -0
  40. package/dist/task-board/components/goalDisplaySpec.js.map +1 -0
  41. package/dist/task-board/goalRefreshState.d.ts +6 -0
  42. package/dist/task-board/goalRefreshState.js +11 -0
  43. package/dist/task-board/goalRefreshState.js.map +1 -0
  44. package/dist/task-board/goalTaskNavigationState.d.ts +43 -0
  45. package/dist/task-board/goalTaskNavigationState.js +65 -0
  46. package/dist/task-board/goalTaskNavigationState.js.map +1 -0
  47. package/dist/task-board/types.d.ts +6 -0
  48. package/package.json +1 -1
@@ -1,11 +1,13 @@
1
1
  import { setContentTreeItemDragImage, setMultiDragImage, } from "../ui/DragPreview";
2
2
  /**
3
- * Payload marker so a media drag can be told apart from a file drag. The real
4
- * payload travels through `editContext.dragObject` (same as the content tree);
5
- * the data transfer entry only exists because Firefox refuses to start a drag
6
- * without one.
3
+ * Payload marker so a media drag can be told apart from a file drag. The
4
+ * application mirrors the descriptor in DataTransfer as well as React state:
5
+ * native drag/drop can outlive or outrun the render that carries dragObject.
7
6
  */
8
7
  export const MEDIA_ITEM_DRAG_MIME = "application/x-parhelia-media-items";
8
+ const POINTER_DRAG_THRESHOLD_PX = 6;
9
+ let pendingPointerDrag;
10
+ let pointerDragCleanup;
9
11
  export function toMediaItemDescriptor(item, fallbackLanguage) {
10
12
  return {
11
13
  id: item.id,
@@ -14,6 +16,68 @@ export function toMediaItemDescriptor(item, fallbackLanguage) {
14
16
  name: item.name,
15
17
  };
16
18
  }
19
+ function createMediaItemDragObject(items, language) {
20
+ if (items.length === 0)
21
+ return undefined;
22
+ return {
23
+ type: "items",
24
+ typeId: items[0]?.templateId ?? "",
25
+ items: items.map((item) => toMediaItemDescriptor(item, language)),
26
+ };
27
+ }
28
+ /**
29
+ * Arms a pointer-based fallback before the browser decides whether to begin a
30
+ * native HTML drag. Native drag is still the primary path. If the browser
31
+ * never emits dragstart (seen with loaded Chromium processes, touch, and pen),
32
+ * ContentTree can consume this gesture on pointerup and perform the same move.
33
+ */
34
+ export function beginMediaItemPointerDrag({ event, items, language, }) {
35
+ if (event.button !== 0 || event.isPrimary === false)
36
+ return;
37
+ cancelMediaItemPointerDrag();
38
+ const dragObject = createMediaItemDragObject(items, language);
39
+ if (!dragObject || typeof window === "undefined")
40
+ return;
41
+ pendingPointerDrag = {
42
+ pointerId: event.pointerId,
43
+ startX: event.clientX,
44
+ startY: event.clientY,
45
+ dragObject,
46
+ };
47
+ const clearAfterPointerUp = (pointerEvent) => {
48
+ if (pointerEvent.pointerId !== pendingPointerDrag?.pointerId)
49
+ return;
50
+ // React handles the tree's pointerup while the event bubbles. Clear on the
51
+ // next task so ContentTree gets the first chance to consume the gesture.
52
+ window.setTimeout(() => cancelMediaItemPointerDrag(), 0);
53
+ };
54
+ const clearOnPointerCancel = (pointerEvent) => {
55
+ if (pointerEvent.pointerId === pendingPointerDrag?.pointerId) {
56
+ cancelMediaItemPointerDrag();
57
+ }
58
+ };
59
+ window.addEventListener("pointerup", clearAfterPointerUp);
60
+ window.addEventListener("pointercancel", clearOnPointerCancel);
61
+ pointerDragCleanup = () => {
62
+ window.removeEventListener("pointerup", clearAfterPointerUp);
63
+ window.removeEventListener("pointercancel", clearOnPointerCancel);
64
+ };
65
+ }
66
+ export function cancelMediaItemPointerDrag() {
67
+ pointerDragCleanup?.();
68
+ pointerDragCleanup = undefined;
69
+ pendingPointerDrag = undefined;
70
+ }
71
+ export function consumeMediaItemPointerDrag(event) {
72
+ const pending = pendingPointerDrag;
73
+ if (!pending || pending.pointerId !== event.pointerId)
74
+ return undefined;
75
+ const distance = Math.hypot(event.clientX - pending.startX, event.clientY - pending.startY);
76
+ if (distance < POINTER_DRAG_THRESHOLD_PX)
77
+ return undefined;
78
+ cancelMediaItemPointerDrag();
79
+ return pending.dragObject;
80
+ }
17
81
  /**
18
82
  * Starts dragging media items out of a thumbnail grid. Mirrors the content
19
83
  * tree's drag start so the tree's existing drop zones light up and can move the
@@ -22,9 +86,15 @@ export function toMediaItemDescriptor(item, fallbackLanguage) {
22
86
  export function startMediaItemDrag({ event, editContext, items, language, }) {
23
87
  if (!editContext || items.length === 0)
24
88
  return;
89
+ // Once native dragstart fires it owns the gesture; disarm the pointer
90
+ // fallback so a later pointerup cannot execute the move twice.
91
+ cancelMediaItemPointerDrag();
92
+ const dragObject = createMediaItemDragObject(items, language);
93
+ if (!dragObject)
94
+ return;
25
95
  try {
26
96
  event.dataTransfer.effectAllowed = "move";
27
- event.dataTransfer.setData(MEDIA_ITEM_DRAG_MIME, items.map((item) => item.id).join(","));
97
+ event.dataTransfer.setData(MEDIA_ITEM_DRAG_MIME, JSON.stringify(dragObject));
28
98
  }
29
99
  catch { }
30
100
  if (items.length > 1) {
@@ -33,10 +103,38 @@ export function startMediaItemDrag({ event, editContext, items, language, }) {
33
103
  else {
34
104
  setContentTreeItemDragImage(event, items[0].name);
35
105
  }
36
- editContext.dragStart({
37
- type: "items",
38
- typeId: items[0]?.templateId ?? "",
39
- items: items.map((item) => toMediaItemDescriptor(item, language)),
40
- });
106
+ editContext.dragStart(dragObject);
107
+ }
108
+ /**
109
+ * Recovers a media item drag from the browser-owned payload when the React
110
+ * drag state has not committed yet or was cleared by an intervening render.
111
+ */
112
+ export function readMediaItemDrag(dataTransfer) {
113
+ if (!dataTransfer)
114
+ return undefined;
115
+ try {
116
+ const raw = dataTransfer.getData(MEDIA_ITEM_DRAG_MIME);
117
+ if (!raw)
118
+ return undefined;
119
+ const parsed = JSON.parse(raw);
120
+ if (parsed.type !== "items" || !Array.isArray(parsed.items)) {
121
+ return undefined;
122
+ }
123
+ const items = parsed.items.filter((item) => !!item &&
124
+ typeof item.id === "string" &&
125
+ item.id.trim().length > 0 &&
126
+ typeof item.language === "string" &&
127
+ Number.isFinite(item.version));
128
+ if (items.length === 0)
129
+ return undefined;
130
+ return {
131
+ type: "items",
132
+ typeId: typeof parsed.typeId === "string" ? parsed.typeId : "",
133
+ items,
134
+ };
135
+ }
136
+ catch {
137
+ return undefined;
138
+ }
41
139
  }
42
140
  //# sourceMappingURL=mediaItemDrag.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"mediaItemDrag.js","sourceRoot":"","sources":["../../../src/editor/media-selector/mediaItemDrag.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,2BAA2B,EAC3B,iBAAiB,GAClB,MAAM,mBAAmB,CAAC;AAE3B;;;;;GAKG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,oCAAoC,CAAC;AAIzE,MAAM,UAAU,qBAAqB,CACnC,IAAsB,EACtB,gBAAwB;IAExB,OAAO;QACL,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,gBAAgB;QAC3C,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,CAAC;QAC1B,IAAI,EAAE,IAAI,CAAC,IAAI;KAChB,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,EACjC,KAAK,EACL,WAAW,EACX,KAAK,EACL,QAAQ,GAMT;IACC,IAAI,CAAC,WAAW,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAE/C,IAAI,CAAC;QACH,KAAK,CAAC,YAAY,CAAC,aAAa,GAAG,MAAM,CAAC;QAC1C,KAAK,CAAC,YAAY,CAAC,OAAO,CACxB,oBAAoB,EACpB,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CACvC,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;IAEV,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACxD,CAAC;SAAM,CAAC;QACN,2BAA2B,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAE,CAAC,IAAI,CAAC,CAAC;IACrD,CAAC;IAED,WAAW,CAAC,SAAS,CAAC;QACpB,IAAI,EAAE,OAAO;QACb,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,UAAU,IAAI,EAAE;QAClC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,qBAAqB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;KAClE,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"mediaItemDrag.js","sourceRoot":"","sources":["../../../src/editor/media-selector/mediaItemDrag.ts"],"names":[],"mappings":"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"}
@@ -16,6 +16,7 @@ import { Switch } from "../../../components/ui/switch";
16
16
  import { Textarea } from "../../../components/ui/textarea";
17
17
  import { cn } from "../../../lib/utils";
18
18
  import { TaskAssigneePicker } from "../../../task-board/components/TaskAssigneePicker";
19
+ import { parseGoalDisplaySpec } from "../../../task-board/components/goalDisplaySpec";
19
20
  import { deleteProjectTemplate, getProjectTemplateAgent, getProjectTemplates, resetProjectTemplateAgent, upsertProjectTemplate, } from "../../../task-board/services/taskService";
20
21
  const DependencyGraphView = lazy(() => import("../../../task-board/views/DependencyGraphView").then((module) => ({
21
22
  default: module.DependencyGraphView,
@@ -236,6 +237,7 @@ function createEmptyGoalDefinition(goalCount, title) {
236
237
  sortOrder: goalCount * 100,
237
238
  defaultScopeJson: null,
238
239
  dataShape: null,
240
+ displaySpec: null,
239
241
  };
240
242
  }
241
243
  function sortTemplates(templates) {
@@ -297,9 +299,19 @@ export function buildUpsertTemplateRequest(template) {
297
299
  sortOrder: goal.sortOrder || index * 100,
298
300
  defaultScopeJson: goal.defaultScopeJson?.trim() || null,
299
301
  dataShape: goal.dataShape?.trim() || null,
302
+ displaySpec: goal.displaySpec?.trim() || null,
300
303
  })),
301
304
  };
302
305
  }
306
+ function getGoalDisplaySpecError(goals) {
307
+ for (const goal of goals ?? []) {
308
+ const result = parseGoalDisplaySpec(goal.displaySpec);
309
+ if ("error" in result) {
310
+ return `${goal.title || "Untitled goal"}: ${result.error}`;
311
+ }
312
+ }
313
+ return null;
314
+ }
303
315
  export function mergeTemplateResponseWithRequest(serverTemplate, request) {
304
316
  const requestedTaskById = new Map((request.taskTemplates ?? []).map((task) => [task.id, task]));
305
317
  const requestedGoalById = new Map((request.goalTemplates ?? []).map((goal) => [goal.id, goal]));
@@ -314,6 +326,9 @@ export function mergeTemplateResponseWithRequest(serverTemplate, request) {
314
326
  return {
315
327
  ...goal,
316
328
  description: requested?.description ?? goal.description,
329
+ displaySpec: requested
330
+ ? (requested.displaySpec ?? null)
331
+ : goal.displaySpec,
317
332
  };
318
333
  }),
319
334
  ...(request.goalTemplates ?? []).filter((goal) => !serverGoalIds.has(goal.id)),
@@ -370,6 +385,8 @@ function getActiveDescriptionDraftField(selectedTaskId, selectedGoalId) {
370
385
  return { kind: "task", id: selectedTaskId };
371
386
  case "project-template-goal-description-input":
372
387
  return { kind: "goal", id: selectedGoalId };
388
+ case "project-template-goal-display-spec-input":
389
+ return { kind: "goalDisplaySpec", id: selectedGoalId };
373
390
  default:
374
391
  return null;
375
392
  }
@@ -408,6 +425,17 @@ function preserveActiveDescriptionDraft(template, draft, activeField) {
408
425
  : goal),
409
426
  };
410
427
  }
428
+ if (activeField.kind === "goalDisplaySpec" && activeField.id) {
429
+ const draftGoal = (draft.goalTemplates ?? []).find((goal) => goal.id === activeField.id);
430
+ if (!draftGoal)
431
+ return template;
432
+ return {
433
+ ...template,
434
+ goalTemplates: (template.goalTemplates ?? []).map((goal) => goal.id === activeField.id
435
+ ? { ...goal, displaySpec: draftGoal.displaySpec }
436
+ : goal),
437
+ };
438
+ }
411
439
  return template;
412
440
  }
413
441
  function isValidGuid(value) {
@@ -518,6 +546,7 @@ function buildProjectTemplateAgentContext(template, selectedTaskTemplateId, sele
518
546
  sortOrder: goal.sortOrder ?? 0,
519
547
  defaultScopeJson: goal.defaultScopeJson?.trim() || null,
520
548
  dataShape: goal.dataShape?.trim() || null,
549
+ displaySpec: goal.displaySpec?.trim() || null,
521
550
  }));
522
551
  const selectedGoal = selectedGoalTemplateId
523
552
  ? (goalDefinitions.find((goal) => goal.id === selectedGoalTemplateId) ??
@@ -1349,6 +1378,14 @@ export function ProjectTemplatesPanel() {
1349
1378
  latestDraftSignatureRef.current = currentSaveSignature;
1350
1379
  }, [currentSaveSignature]);
1351
1380
  const persistTemplate = useCallback(async (request, signature, options) => {
1381
+ const displaySpecError = getGoalDisplaySpecError(request.goalTemplates);
1382
+ if (displaySpecError) {
1383
+ const message = `Invalid goal Display Spec — ${displaySpecError}`;
1384
+ setSaveError(message);
1385
+ if (options?.showErrorToast)
1386
+ toast.error(message);
1387
+ return false;
1388
+ }
1352
1389
  const runId = ++saveRunIdRef.current;
1353
1390
  setSaving(true);
1354
1391
  setSaveError(null);
@@ -2502,7 +2539,14 @@ export function ProjectTemplatesPanel() {
2502
2539
  })), placeholder: '{"rootItemId":"...","pagesOnly":true}' })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { className: "text-xs", htmlFor: "project-template-goal-data-shape-input", children: "Data shape hint (optional)" }), _jsx(Input, { id: "project-template-goal-data-shape-input", className: "text-xs md:text-xs", value: selectedGoal.dataShape ?? "", onChange: (event) => updateSelectedGoal((goal) => ({
2503
2540
  ...goal,
2504
2541
  dataShape: event.target.value,
2505
- })), placeholder: "e.g. { recommendations: string[] }" })] }), _jsxs("div", { className: "flex items-center justify-between gap-4", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "text-neutral-grey-100 text-xs font-medium", children: "Disabled" }), _jsx("div", { className: "text-neutral-grey-50 mt-0.5 text-xs", children: "Keeps the definition but hides it from the runtime goal catalog." })] }), _jsx(Switch, { checked: selectedGoal.disabled === true, onCheckedChange: (checked) => updateSelectedGoal((goal) => ({
2542
+ })), placeholder: "e.g. { recommendations: string[] }" })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { className: "text-xs", htmlFor: "project-template-goal-display-spec-input", children: "Display Spec (JSON, optional)" }), _jsx(Textarea, { id: "project-template-goal-display-spec-input", "data-testid": "project-template-goal-display-spec-input", className: "font-mono text-xs", rows: 5, value: selectedGoal.displaySpec ?? "", onChange: (event) => updateSelectedGoal((goal) => ({
2543
+ ...goal,
2544
+ displaySpec: event.target.value,
2545
+ })), placeholder: '{"version":1,"sections":[{"widget":"summary"}]}', "aria-invalid": "error" in
2546
+ parseGoalDisplaySpec(selectedGoal.displaySpec) }), (() => {
2547
+ const result = parseGoalDisplaySpec(selectedGoal.displaySpec);
2548
+ return "error" in result ? (_jsx("div", { className: "text-feedback-red text-xs", "data-testid": "project-template-goal-display-spec-error", children: result.error })) : null;
2549
+ })()] }), _jsxs("div", { className: "flex items-center justify-between gap-4", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "text-neutral-grey-100 text-xs font-medium", children: "Disabled" }), _jsx("div", { className: "text-neutral-grey-50 mt-0.5 text-xs", children: "Keeps the definition but hides it from the runtime goal catalog." })] }), _jsx(Switch, { checked: selectedGoal.disabled === true, onCheckedChange: (checked) => updateSelectedGoal((goal) => ({
2506
2550
  ...goal,
2507
2551
  disabled: checked,
2508
2552
  })), "aria-label": "Disable this goal definition", "data-testid": "project-template-goal-disabled-switch" })] })] })) : (_jsx("div", { className: "border-border-default text-neutral-grey-50 flex items-center justify-center rounded-md border border-dashed p-6 text-center text-xs", children: "Select a goal to edit its details." }))] })) }));