@plannotator/ui 0.50.0 → 0.51.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.
Files changed (53) hide show
  1. package/HANDOFF.md +144 -11
  2. package/components/AISettingsTab.tsx +32 -0
  3. package/components/AgentToolAnnouncementDialog.tsx +347 -0
  4. package/components/AgentsTab.tsx +9 -9
  5. package/components/AskSessionAnnouncementDialog.tsx +329 -0
  6. package/components/CommentPopover.tsx +32 -18
  7. package/components/DecisionControl.tsx +2 -7
  8. package/components/DiagramBlock.tsx +46 -4
  9. package/components/DocBadges.tsx +12 -8
  10. package/components/PlanHeaderMenu.tsx +1 -1
  11. package/components/Settings.tsx +73 -1
  12. package/components/TerminalToolsAnnouncementDialog.tsx +1 -1
  13. package/components/Viewer.tsx +4 -0
  14. package/components/ai/AIProviderBar.tsx +18 -1
  15. package/components/ai/DocumentAIChatPanel.tsx +4 -9
  16. package/components/ai/SessionAskNotice.tsx +33 -21
  17. package/components/blocks/QuestionBlock.tsx +24 -19
  18. package/components/blocks/QuestionContext.tsx +81 -0
  19. package/components/diagram/DiagramComposer.tsx +54 -3
  20. package/components/diagram/DiagramViewer.tsx +31 -2
  21. package/components/diagram/index.ts +1 -1
  22. package/components/html-viewer/HtmlViewer.tsx +13 -3
  23. package/components/html-viewer/bridge-script.asset.js +124 -8
  24. package/components/html-viewer/bridge-script.ts +124 -8
  25. package/components/html-viewer/useHtmlAnnotation.ts +36 -55
  26. package/hooks/useAIChat.ts +26 -5
  27. package/hooks/useAIProviderActivation.ts +13 -0
  28. package/hooks/useAIProviderConfig.ts +3 -3
  29. package/hooks/useActiveSection.ts +3 -2
  30. package/hooks/useAgentToolSetting.ts +33 -0
  31. package/hooks/useAnnotationDraft.ts +53 -7
  32. package/hooks/useAnnotationHighlighter.ts +101 -5
  33. package/hooks/useExternalAnnotations.ts +21 -1
  34. package/hooks/useFirstRunAnnouncementWindow.ts +114 -0
  35. package/hooks/useLatchedTrue.ts +15 -0
  36. package/hooks/useLinkedDoc.ts +64 -4
  37. package/hooks/useModelCatalogs.ts +9 -2
  38. package/package.json +2 -2
  39. package/styles.css +1 -1
  40. package/themes/gruvbox.css +6 -0
  41. package/types.ts +13 -0
  42. package/utils/agentClosed.ts +33 -0
  43. package/utils/agentToolAnnouncement.ts +109 -0
  44. package/utils/agentToolSetting.ts +108 -0
  45. package/utils/aiProvider.ts +13 -40
  46. package/utils/annotationOwnsHighlight.ts +27 -0
  47. package/utils/annotationScope.ts +15 -0
  48. package/utils/askSessionAnnouncement.ts +107 -0
  49. package/utils/blockTargeting.ts +61 -2
  50. package/utils/diagramAskAI.ts +161 -0
  51. package/utils/parser.ts +236 -110
  52. package/utils/questionAnswers.ts +3 -1
  53. package/utils/quickLabels.ts +2 -2
@@ -14,6 +14,7 @@ import type { ViewerHandle } from "../components/Viewer";
14
14
  import type { SidebarTab } from "./useSidebar";
15
15
  import type { SourceSaveCapability } from "@plannotator/core/source-save";
16
16
  import type { VersionInfo } from "./usePlanDiff";
17
+ import { annotationOwnsHighlight } from "../utils/annotationOwnsHighlight";
17
18
 
18
19
  export interface LinkedDocLoadData {
19
20
  markdown?: string;
@@ -124,6 +125,29 @@ export interface CachedDocState {
124
125
  versionInfo?: VersionInfo | null;
125
126
  }
126
127
 
128
+ /**
129
+ * The session's documents split the way FEEDBACK must be: the root document
130
+ * (the plan, the annotated file, or the folder itself) once, and every other
131
+ * document once under its own path. `getDocAnnotations()` is NOT this split —
132
+ * it also carries the active document's live state, which the host exports
133
+ * from its own state too, so exporting both printed the open document twice.
134
+ */
135
+ export interface FeedbackDocuments {
136
+ /**
137
+ * The root document's stashed state while a linked document is open. Null
138
+ * while the root itself is the active document: its annotations are then
139
+ * the host's live state, which the host exports itself.
140
+ */
141
+ root: (CachedDocState & { renderAs: DocumentRenderAs }) | null;
142
+ /**
143
+ * Every document other than the root that carries state, keyed by path:
144
+ * the cached documents plus the active linked document's live state. The
145
+ * root's own annotations are never here; a copy of the root's path opened
146
+ * from the root itself is a separate annotation set and is listed by path.
147
+ */
148
+ documents: Map<string, CachedDocState>;
149
+ }
150
+
127
151
  export interface LinkedDocSessionState {
128
152
  root: SavedPlanState;
129
153
  docs: Map<string, CachedDocState>;
@@ -156,8 +180,12 @@ export interface UseLinkedDocReturn {
156
180
  back: () => void;
157
181
  /** Dismiss the current error */
158
182
  dismissError: () => void;
159
- /** All linked doc annotations including the active doc's live state (keyed by filepath) */
183
+ /** All linked doc annotations including the active doc's live state (keyed by filepath).
184
+ * For counts and the cross-file panel; a feedback export reads getFeedbackDocuments(). */
160
185
  getDocAnnotations: () => Map<string, CachedDocState>;
186
+ /** The root document and every other document, each exactly once — what a
187
+ * feedback export reads (see FeedbackDocuments). */
188
+ getFeedbackDocuments: () => FeedbackDocuments;
161
189
  /**
162
190
  * Replace the stored annotations of a document that is NOT the active one —
163
191
  * a cached linked doc, or the stashed source document. This is what lets the
@@ -285,7 +313,7 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn {
285
313
  if (saved.annotations.length) {
286
314
  setTimeout(() => {
287
315
  viewerRef.current?.clearAllHighlights();
288
- viewerRef.current?.applySharedAnnotations(saved.annotations);
316
+ viewerRef.current?.applySharedAnnotations(saved.annotations.filter(annotationOwnsHighlight));
289
317
  }, HIGHLIGHT_REAPPLY_DELAY);
290
318
  }
291
319
  }, [
@@ -415,7 +443,7 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn {
415
443
  if (cached?.annotations.length) {
416
444
  setTimeout(() => {
417
445
  viewerRef.current?.clearAllHighlights();
418
- viewerRef.current?.applySharedAnnotations(cached.annotations);
446
+ viewerRef.current?.applySharedAnnotations(cached.annotations.filter(annotationOwnsHighlight));
419
447
  }, HIGHLIGHT_REAPPLY_DELAY);
420
448
  }
421
449
  }, [
@@ -562,7 +590,7 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn {
562
590
  if (state.root.annotations.length) {
563
591
  setTimeout(() => {
564
592
  viewerRef.current?.clearAllHighlights();
565
- viewerRef.current?.applySharedAnnotations(state.root.annotations);
593
+ viewerRef.current?.applySharedAnnotations(state.root.annotations.filter(annotationOwnsHighlight));
566
594
  }, HIGHLIGHT_REAPPLY_DELAY);
567
595
  }
568
596
  }, [
@@ -644,6 +672,37 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn {
644
672
  // identity of this callback is what tells memoized readers to recompute.
645
673
  }, [linkedDoc, annotations, globalAttachments, sourceFilePath, sourceConverted, getDocumentMarkdown, storeRevision]);
646
674
 
675
+ const getFeedbackDocuments = useCallback((): FeedbackDocuments => {
676
+ const documents = new Map(docCache.current);
677
+ if (linkedDoc) {
678
+ documents.set(linkedDoc.filepath, {
679
+ annotations: [...annotations],
680
+ globalAttachments: [...globalAttachments],
681
+ markdown: getDocumentMarkdown?.(linkedDoc.filepath, linkedDoc.markdown) ?? linkedDoc.markdown,
682
+ isConverted: linkedDoc.isConverted,
683
+ });
684
+ }
685
+ // A cache entry under the root's own path is NOT the root: it is a copy
686
+ // the reviewer opened from the root itself (an HTML "Home" link, or
687
+ // index.html#intro from index.html — a backlink only routes through
688
+ // back() while a linked doc is open). Its comments are a separate set the
689
+ // counts include, so it is exported under its path, never dropped.
690
+ const saved = linkedDoc ? savedPlanState.current : null;
691
+ return {
692
+ root: saved
693
+ ? {
694
+ annotations: [...saved.annotations],
695
+ globalAttachments: [...saved.globalAttachments],
696
+ markdown: saved.markdown,
697
+ isConverted: !!sourceConverted,
698
+ renderAs: saved.renderAs,
699
+ }
700
+ : null,
701
+ documents,
702
+ };
703
+ // storeRevision: same reason as getDocAnnotations above.
704
+ }, [linkedDoc, annotations, globalAttachments, sourceConverted, getDocumentMarkdown, storeRevision]);
705
+
647
706
  return {
648
707
  isActive: linkedDoc !== null,
649
708
  filepath: linkedDoc?.filepath ?? null,
@@ -654,6 +713,7 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn {
654
713
  back,
655
714
  dismissError,
656
715
  getDocAnnotations,
716
+ getFeedbackDocuments,
657
717
  updateStoredAnnotations,
658
718
  snapshotSession,
659
719
  restoreSession,
@@ -14,7 +14,10 @@ export { effortSelectOptions, modelSelectOptions } from '@plannotator/core/model
14
14
  * Claude / Codex model catalogs for the agent launchers (review, Code Tour,
15
15
  * Guided Review) — the SAME lists Ask AI shows. Each is the Ask AI provider's
16
16
  * discovered list, fetched through `/api/ai/capabilities?activate=<id>` (which
17
- * runs the provider's server-cached discovery). Nothing is fetched until a
17
+ * runs the provider's server-cached discovery). When a session bridge makes
18
+ * Ask AI bridge-only, the server reports these under `catalogProviders`
19
+ * instead of `providers`, so the launchers keep them while Ask AI cannot
20
+ * offer them. Nothing is fetched until a
18
21
  * surface asks for one engine with `useModelCatalogs(...).load(engine)`, and
19
22
  * only when that engine is installed, so opening a launcher spawns only the
20
23
  * selected CLI. While a fetch is in flight the static fallback is shown with
@@ -67,7 +70,11 @@ function loadCatalogEntry(engine: CatalogEngine): Promise<LoadedCatalog> {
67
70
  load = fetch(`/api/ai/capabilities?activate=${encodeURIComponent(id)}`)
68
71
  .then((res) => (res.ok ? res.json() : null))
69
72
  .then((data): LoadedCatalog => {
70
- const provider = data?.providers?.find((p: { id?: string; name?: string }) => p.id === id || p.name === id);
73
+ // A server whose Ask AI is a session bridge lists the SDK providers
74
+ // only under `catalogProviders` (never under `providers`, which Ask AI
75
+ // reads), so the launchers look in both.
76
+ const matches = (p: { id?: string; name?: string }) => p.id === id || p.name === id;
77
+ const provider = data?.providers?.find(matches) ?? data?.catalogProviders?.find(matches);
71
78
  const models = provider?.models;
72
79
  if (!Array.isArray(models) || models.length === 0) return failed();
73
80
  // The server answers 200 with its static fallback when discovery
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plannotator/ui",
3
- "version": "0.50.0",
3
+ "version": "0.51.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -77,7 +77,7 @@
77
77
  "@lezer/highlight": "^1.2.3",
78
78
  "@pierre/diffs": "1.3.6",
79
79
  "@plannotator/atomic-editor": "^0.9.0",
80
- "@plannotator/core": "0.25.10",
80
+ "@plannotator/core": "0.25.11",
81
81
  "@plannotator/markdown-editor": "^0.5.0",
82
82
  "@plannotator/web-highlighter": "^0.8.1",
83
83
  "@tanstack/react-table": "^8.21.3",