@plannotator/ui 0.52.1 → 0.53.1
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.
- package/HANDOFF.md +284 -29
- package/README.md +45 -5
- package/components/AnnotationPanel.tsx +195 -118
- package/components/AnnotationToolbar.tsx +17 -14
- package/components/AnnotationToolstrip.tsx +54 -40
- package/components/BlockRenderer.tsx +20 -14
- package/components/CodeFilePopout.tsx +132 -69
- package/components/CommentPopover.tsx +8 -349
- package/components/ComposerTokenTextarea.tsx +379 -0
- package/components/ExportModal.tsx +6 -1
- package/components/HostTitle.tsx +108 -0
- package/components/HtmlSurfaceControls.tsx +2 -1
- package/components/ImageLightbox.tsx +288 -17
- package/components/ImagePeek.tsx +255 -0
- package/components/ImageResolverContext.tsx +31 -0
- package/components/InlineMarkdown.tsx +123 -15
- package/components/LocalLinkContext.tsx +49 -0
- package/components/MentionPicker.tsx +51 -0
- package/components/MenuVersionSection.tsx +51 -25
- package/components/OpenInAppButton.tsx +18 -49
- package/components/PlanHeaderMenu.tsx +2 -8
- package/components/ProviderIcons.tsx +10 -4
- package/components/RenderedMarkdown.tsx +40 -4
- package/components/Settings.tsx +33 -5
- package/components/SkillReferenceMenu.tsx +67 -18
- package/components/Tooltip.tsx +51 -4
- package/components/Viewer.tsx +220 -49
- package/components/ai/AIProviderBar.tsx +11 -0
- package/components/ai/AskSessionSetupHint.tsx +12 -0
- package/components/ai/DocumentAIChatPanel.tsx +17 -5
- package/components/ai/SessionAskNotice.tsx +4 -3
- package/components/blocks/BlockquoteBlock.tsx +159 -0
- package/components/blocks/HtmlBlock.tsx +121 -9
- package/components/blocks/QuestionBlock.tsx +183 -96
- package/components/blocks/QuestionContext.tsx +3 -0
- package/components/diagram/DiagramViewer.tsx +41 -4
- package/components/diagram/index.ts +1 -1
- package/components/html-viewer/HtmlViewer.tsx +95 -3
- package/components/html-viewer/bridge-script.asset.js +413 -7
- package/components/html-viewer/bridge-script.lite.ts +1 -1
- package/components/html-viewer/bridge-script.ts +454 -7
- package/components/html-viewer/useHtmlAnnotation.ts +51 -3
- package/components/icons/AgentIcons.tsx +9 -7
- package/components/plan-diff/PlanCleanDiffView.tsx +239 -36
- package/components/plan-diff/PlanDiffViewer.tsx +6 -1
- package/configure.ts +27 -1
- package/hooks/useAgentJobs.ts +8 -3
- package/hooks/useAnnotationHighlighter.ts +128 -23
- package/hooks/useExternalAnnotations.ts +6 -0
- package/hooks/useMentionAutocomplete.ts +1 -1
- package/hooks/useOpenInApps.ts +88 -0
- package/hooks/useSkillReferenceAutocomplete.ts +7 -3
- package/hooks/useUpdateCheck.ts +38 -98
- package/hooks/useUpdateCheckSetting.ts +43 -0
- package/hooks/useUpdateToast.ts +101 -0
- package/hooks/useVimSelection.ts +9 -0
- package/package.json +2 -2
- package/shortcuts/code-review/markdownPreview.shortcuts.ts +18 -0
- package/shortcuts/index.ts +1 -0
- package/shortcuts/runtime.ts +10 -3
- package/styles.css +1 -1
- package/types.ts +48 -0
- package/utils/agentClosed.ts +26 -0
- package/utils/agentToolSetting.ts +16 -9
- package/utils/aiProvider.ts +7 -0
- package/utils/askSessionSetupHint.ts +27 -0
- package/utils/blockTargeting.ts +14 -3
- package/utils/mentions.ts +34 -5
- package/utils/parser.ts +65 -51
- package/utils/pasteInsert.ts +234 -0
- package/utils/planDiffEngine.ts +272 -2
- package/utils/skillCatalog.ts +15 -2
- package/utils/skillReferences.ts +50 -13
- package/utils/storage.ts +5 -0
- package/utils/textCut.ts +41 -0
- package/utils/updateNotice.ts +120 -0
package/components/Viewer.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { generateId } from '../utils/generateId';
|
|
2
2
|
import React, { useRef, useState, useEffect, useLayoutEffect, useMemo, forwardRef, useImperativeHandle, useCallback, lazy, Suspense } from 'react';
|
|
3
3
|
import { createPortal } from 'react-dom';
|
|
4
|
-
import { ImageLightbox } from './ImageLightbox';
|
|
5
|
-
import { AnnotationType, type Block, type Annotation, type EditorMode, type InputMethod, type ImageAttachment, type ActionsLabelMode } from '../types';
|
|
4
|
+
import { collectDocumentImages, ImageLightbox, type LightboxImage } from './ImageLightbox';
|
|
5
|
+
import { AnnotationType, type Block, type Annotation, type EditorMode, type InputMethod, type ImageAttachment, type ActionsLabelMode, type HostDraft } from '../types';
|
|
6
6
|
import { applyHighlight, codeBlockClassName, onCodeHighlightSwap } from '../utils/codeHighlight';
|
|
7
7
|
import { paintCodeBlockMark } from '../utils/codeBlockMark';
|
|
8
8
|
import { useFenceTheme } from '../hooks/useFenceTheme';
|
|
@@ -11,12 +11,14 @@ import { buildHeadingSlugMap } from '../utils/slugify';
|
|
|
11
11
|
import { copyTextToClipboard } from '../utils/clipboard';
|
|
12
12
|
import { BlockRenderer } from './BlockRenderer';
|
|
13
13
|
import { indexQuestionBlocks, type IndexedQuestion, type ParsedQuestion, type QuestionAnswer } from '@plannotator/core/question-block';
|
|
14
|
+
import type { QuestionBlockProps } from './blocks/QuestionBlock';
|
|
14
15
|
import { resolveQuestionAnswers } from '../utils/questionAnswers';
|
|
15
16
|
import { CodeBlock } from './blocks/CodeBlock';
|
|
16
17
|
import { TableBlock } from './blocks/TableBlock';
|
|
17
18
|
import { TableToolbar } from './blocks/TableToolbar';
|
|
18
19
|
import { TablePopout } from './blocks/TablePopout';
|
|
19
20
|
import { CodePathValidationContext } from './CodePathValidationContext';
|
|
21
|
+
import { LocalLinkContext, type LocalLinkResolver } from './LocalLinkContext';
|
|
20
22
|
import { useValidatedCodePaths } from '../hooks/useValidatedCodePaths';
|
|
21
23
|
import { AnnotationToolbar } from './AnnotationToolbar';
|
|
22
24
|
import { FloatingQuickLabelPicker } from './FloatingQuickLabelPicker';
|
|
@@ -132,6 +134,13 @@ export interface ViewerProps {
|
|
|
132
134
|
onAddGlobalAttachment?: (image: ImageAttachment) => void;
|
|
133
135
|
onRemoveGlobalAttachment?: (path: string) => void;
|
|
134
136
|
repoInfo?: { display: string; branch?: string; host?: string } | null;
|
|
137
|
+
/**
|
|
138
|
+
* The repository bare `#123` / `@user` refs link into, WITHOUT the repo
|
|
139
|
+
* badge `repoInfo` also draws (a host that renders many small documents,
|
|
140
|
+
* such as the Inbox's messages). Wins over `repoInfo` for links only;
|
|
141
|
+
* `null` and absent both fall back to `repoInfo`, as before.
|
|
142
|
+
*/
|
|
143
|
+
refRepo?: { display: string; host?: string } | null;
|
|
135
144
|
stickyActions?: boolean;
|
|
136
145
|
/**
|
|
137
146
|
* Render compact annotation controls and the existing document actions in
|
|
@@ -145,7 +154,27 @@ export interface ViewerProps {
|
|
|
145
154
|
gridEnabled?: boolean;
|
|
146
155
|
onOpenLinkedDoc?: (path: string) => void;
|
|
147
156
|
onOpenCodeFile?: (path: string) => void;
|
|
157
|
+
/**
|
|
158
|
+
* Opt-in host capability: decide what a link to a local path does
|
|
159
|
+
* (`[the mock](mock.html)`, markdown and raw-HTML `<a href>` alike; never
|
|
160
|
+
* `#anchors`, `//host` or a URL with a scheme). Return `{ kind: 'open' }`
|
|
161
|
+
* to open it yourself, `{ kind: 'unavailable', title }` to draw it as
|
|
162
|
+
* inert text, or null for the default. Absent → unchanged.
|
|
163
|
+
*/
|
|
164
|
+
resolveLocalLink?: LocalLinkResolver;
|
|
148
165
|
imageBaseDir?: string;
|
|
166
|
+
/**
|
|
167
|
+
* A click on an image in the document. Given: the host handles it (the
|
|
168
|
+
* Inbox opens a message's image in its own viewer) and no lightbox opens.
|
|
169
|
+
* Absent: the lightbox, as always.
|
|
170
|
+
*/
|
|
171
|
+
onImageClick?: (src: string, alt: string) => void;
|
|
172
|
+
/**
|
|
173
|
+
* The lightbox steps through every image the document shows (← →, the
|
|
174
|
+
* peek; approval "inbox-image-viewer": plan review and annotate's light
|
|
175
|
+
* version). Absent or false: the single image overlay, as always.
|
|
176
|
+
*/
|
|
177
|
+
imageLightboxStepping?: boolean;
|
|
149
178
|
/** Directory the active document lives in — used by the code-path validator
|
|
150
179
|
* so out-of-tree relative references (e.g. `../foo.ts` in a linked doc)
|
|
151
180
|
* resolve against the doc's own directory rather than only cwd. */
|
|
@@ -232,6 +261,11 @@ export interface ViewerProps {
|
|
|
232
261
|
* `Decision: when answered`. `'any'`: also a question with no decision
|
|
233
262
|
* line, when `questionDecisionRecording` returns a boolean for it. */
|
|
234
263
|
questionDecisionScope?: 'when-answered' | 'any';
|
|
264
|
+
/** Whether `:::question` cards read `Decision:` lines (default true). False
|
|
265
|
+
* (a host that records no decisions, such as the Plannotator Inbox): a
|
|
266
|
+
* `Decision:` line is shown as context and no question carries a decision
|
|
267
|
+
* tag or row from it. The prompt, key and numbering are unchanged. */
|
|
268
|
+
questionDecisionLines?: boolean;
|
|
235
269
|
/** Makes a question's "Records a decision" tag a toggle button
|
|
236
270
|
* (`aria-pressed`); a click calls this with the question's key and the
|
|
237
271
|
* next state, which the host stores and returns from
|
|
@@ -243,10 +277,35 @@ export interface ViewerProps {
|
|
|
243
277
|
* (anchor your popover to it). Offered on and off. Ignored when
|
|
244
278
|
* `readOnly`. */
|
|
245
279
|
onOpenQuestionDecision?: (key: string, anchor: HTMLElement) => void;
|
|
280
|
+
/** Per question, true draws its decision tag as a plain tag: no switch and
|
|
281
|
+
* no opener, as under `readOnly`, while the other questions keep theirs
|
|
282
|
+
* (a host whose message holds one sent and one open question). Absent:
|
|
283
|
+
* every question follows `readOnly`. */
|
|
284
|
+
questionDecisionLocked?: (question: IndexedQuestion) => boolean;
|
|
285
|
+
/** Per question, true draws that card read-only, as under `readOnly` (no
|
|
286
|
+
* picks, no Other, no note, no Skip, no Accept recommended, and its
|
|
287
|
+
* decision tag a plain tag), while the other questions stay answerable (a
|
|
288
|
+
* host whose message holds one sent and one open question). Absent: every
|
|
289
|
+
* question follows `readOnly`. */
|
|
290
|
+
questionReadOnly?: (question: IndexedQuestion) => boolean;
|
|
246
291
|
/** `'none'` hides each question card's own status tag (Open / Answered /
|
|
247
292
|
* Settled / Skipped) so the host can draw its own; the decision tags stay.
|
|
248
293
|
* Default `'card'`. */
|
|
249
294
|
questionStatusTag?: 'card' | 'none';
|
|
295
|
+
/** The host's own words for a question's status tag (ui seam, see
|
|
296
|
+
* `QuestionBlock` `statusLabel`): the Plannotator Inbox says "Not sent" for
|
|
297
|
+
* a saved, unsent pick. Absent: the card's own tags. */
|
|
298
|
+
questionStatusLabel?: QuestionBlockProps['statusLabel'];
|
|
299
|
+
/** A host's own card in place of a question's (ui seam for a question the
|
|
300
|
+
* host no longer lets anyone answer, such as the Plannotator Inbox's
|
|
301
|
+
* replaced questions). Called per question; a returned node is drawn
|
|
302
|
+
* instead of the answer card, inside a wrapper carrying the block's
|
|
303
|
+
* `data-block-id`; `undefined` keeps the answer card. Absent: every
|
|
304
|
+
* question draws its answer card (today's behaviour). */
|
|
305
|
+
renderQuestionCard?: (question: IndexedQuestion, answer: QuestionAnswer | undefined) => React.ReactNode | undefined;
|
|
306
|
+
/** Host content right under a question card's prompt, per question (e.g.
|
|
307
|
+
* "Replaces <an earlier question>"); null for none. Absent: nothing there. */
|
|
308
|
+
renderQuestionBelowPrompt?: (question: IndexedQuestion) => React.ReactNode;
|
|
250
309
|
onAskAI?: CommentAskAIHandler;
|
|
251
310
|
/** Whether comment popovers offer image attachments. Hosts without an
|
|
252
311
|
* uploadTransport pass false so the attach affordance never dead-ends.
|
|
@@ -257,6 +316,28 @@ export interface ViewerProps {
|
|
|
257
316
|
* comment, attachments, checkbox toggles). Existing annotations still
|
|
258
317
|
* render and remain selectable. Default false — today's behavior. */
|
|
259
318
|
readOnly?: boolean;
|
|
319
|
+
/**
|
|
320
|
+
* Opt-in host capability: the document is read, not annotated. Every
|
|
321
|
+
* annotation affordance is off exactly as under `readOnly` (selection and
|
|
322
|
+
* code-block toolbars, comment composers, pinpoint, the annotation header,
|
|
323
|
+
* global comment and attachments, Vim), the document actions (Global
|
|
324
|
+
* comment, Copy) are not drawn, but question cards and checkboxes keep their
|
|
325
|
+
* handlers. For a host that shows a message with answerable `:::question`
|
|
326
|
+
* blocks and no comments on its text. Absent → unchanged.
|
|
327
|
+
*/
|
|
328
|
+
answerOnly?: boolean;
|
|
329
|
+
/**
|
|
330
|
+
* Opt-in host capability, with `answerOnly`: the reader may comment on the
|
|
331
|
+
* text. A text selection opens the selection toolbar, comment-only
|
|
332
|
+
* (Comment and the one-click 👍; no Delete, no label picker), and its
|
|
333
|
+
* comment composer; `annotations` are drawn as highlights. Question cards
|
|
334
|
+
* are left out of selection (their text is not commented on), and nothing
|
|
335
|
+
* else of authoring comes back (no pinpoint, code-block toolbar, Global
|
|
336
|
+
* comment or Copy, no Vim). For a host that shows a message whose words
|
|
337
|
+
* can be commented on (the Plannotator Inbox's agent messages). Ignored
|
|
338
|
+
* without `answerOnly` and under `readOnly`. Absent → unchanged.
|
|
339
|
+
*/
|
|
340
|
+
textComments?: boolean;
|
|
260
341
|
/** Opt-in Vim-style keyboard selection. Default false for compatibility. */
|
|
261
342
|
vimModeEnabled?: boolean;
|
|
262
343
|
/** Replace the compact Vim badge with the live video-style key HUD. */
|
|
@@ -268,12 +349,36 @@ export interface ViewerProps {
|
|
|
268
349
|
/** Fires once per highlight-restore pass with what it tried and what it could
|
|
269
350
|
* not anchor, so a host can mark the leftovers in its annotation panel. */
|
|
270
351
|
onRestoreReport?: (report: AnnotationRestoreReport) => void;
|
|
352
|
+
/**
|
|
353
|
+
* Opt-in host capability: the composer handed to the host (see
|
|
354
|
+
* `HostDraft`). A selection settling, a pinpoint tap or Comment mode reports
|
|
355
|
+
* a draft here instead of opening the toolbar or a composer (a pinpoint tap
|
|
356
|
+
* reports `intent: 'compose'`), and null when it closes. The document
|
|
357
|
+
* actions (Global comment, Copy) and the code-block and table hover
|
|
358
|
+
* toolbars are not drawn, and a pinpoint tap on a code block does nothing.
|
|
359
|
+
* The host saves the comment and draws it through `annotations` /
|
|
360
|
+
* `applySharedAnnotations`. Ignored when authoring is off. Absent →
|
|
361
|
+
* unchanged.
|
|
362
|
+
*/
|
|
363
|
+
onHostDraft?: (draft: HostDraft | null) => void;
|
|
271
364
|
}
|
|
272
365
|
|
|
273
366
|
export interface ViewerHandle {
|
|
274
367
|
removeHighlight: (id: string) => void;
|
|
275
368
|
clearAllHighlights: () => void;
|
|
276
369
|
applySharedAnnotations: (annotations: Annotation[]) => void;
|
|
370
|
+
/** `HtmlViewer` only: move a pinned draft to the element around it
|
|
371
|
+
* (`'parent'`) or back in (`'child'`), for a host that composes
|
|
372
|
+
* (`onHostDraft`). The markdown `Viewer` does not implement it. */
|
|
373
|
+
stepPin?: (direction: 'parent' | 'child') => void;
|
|
374
|
+
/** `HtmlViewer` only: scroll the pinned draft into the middle of the
|
|
375
|
+
* frame, for a host whose panel covers part of it. */
|
|
376
|
+
revealPin?: () => void;
|
|
377
|
+
/** Markdown `Viewer` only, for a host that composes (`onHostDraft`) on a
|
|
378
|
+
* touch screen: make the page's text selection a draft now (the host's own
|
|
379
|
+
* Comment action), reported through `onHostDraft` as `'selection'`.
|
|
380
|
+
* Returns false when nothing in the document is selected. */
|
|
381
|
+
takeSelection?: () => boolean;
|
|
277
382
|
}
|
|
278
383
|
|
|
279
384
|
interface CodeBlockToolbarTarget {
|
|
@@ -588,6 +693,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
588
693
|
onAddGlobalAttachment,
|
|
589
694
|
onRemoveGlobalAttachment,
|
|
590
695
|
repoInfo,
|
|
696
|
+
refRepo,
|
|
591
697
|
stickyActions = true,
|
|
592
698
|
annotationHeader,
|
|
593
699
|
gridEnabled = false,
|
|
@@ -601,8 +707,11 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
601
707
|
maxWidth,
|
|
602
708
|
onOpenLinkedDoc,
|
|
603
709
|
onOpenCodeFile,
|
|
710
|
+
resolveLocalLink,
|
|
604
711
|
linkedDocInfo,
|
|
605
712
|
imageBaseDir,
|
|
713
|
+
onImageClick: hostImageClick,
|
|
714
|
+
imageLightboxStepping = false,
|
|
606
715
|
codePathBaseDir,
|
|
607
716
|
disableCodePathValidation,
|
|
608
717
|
onRestoreReport,
|
|
@@ -621,21 +730,50 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
621
730
|
renderQuestionFooter,
|
|
622
731
|
questionDecisionRecording,
|
|
623
732
|
questionDecisionScope,
|
|
733
|
+
questionDecisionLines = true,
|
|
624
734
|
onToggleQuestionDecisionRecording,
|
|
625
735
|
onOpenQuestionDecision,
|
|
736
|
+
questionDecisionLocked,
|
|
737
|
+
questionReadOnly,
|
|
626
738
|
questionStatusTag,
|
|
739
|
+
questionStatusLabel,
|
|
740
|
+
renderQuestionCard,
|
|
741
|
+
renderQuestionBelowPrompt,
|
|
627
742
|
onAskAI,
|
|
628
743
|
allowImages = true,
|
|
629
744
|
readOnly = false,
|
|
745
|
+
answerOnly = false,
|
|
746
|
+
textComments = false,
|
|
630
747
|
vimModeEnabled = false,
|
|
631
748
|
vimHudEnabled = false,
|
|
632
749
|
vimHudKeyPanelEnabled = true,
|
|
633
750
|
onVimHudKeyPanelChange,
|
|
751
|
+
onHostDraft,
|
|
634
752
|
}, ref) => {
|
|
635
|
-
|
|
753
|
+
// Annotation authoring is off when read-only, and when the host only wants
|
|
754
|
+
// answers (`answerOnly`); question cards and checkboxes follow `readOnly`.
|
|
755
|
+
const authoringOff = readOnly || answerOnly;
|
|
756
|
+
// `textComments`: an answer-only document whose words still take comments
|
|
757
|
+
// (the selection toolbar, comment-only, and its composer); everything else
|
|
758
|
+
// of authoring stays off. Without it this is exactly `!authoringOff`.
|
|
759
|
+
const textCommentsOnly = answerOnly && textComments && !readOnly;
|
|
760
|
+
const selectionCommentsOn = !authoringOff || textCommentsOnly;
|
|
761
|
+
const hostComposes = onHostDraft !== undefined && !authoringOff;
|
|
762
|
+
const viewerAnnotationHeader = authoringOff ? undefined : annotationHeader;
|
|
636
763
|
const hasViewerAnnotationHeader = viewerAnnotationHeader !== undefined;
|
|
764
|
+
// Where `#123` / `@user` link: `refRepo` when the host gave one, else `repoInfo`.
|
|
765
|
+
const linkRepo = refRepo ?? repoInfo;
|
|
637
766
|
const [copied, setCopied] = useState(false);
|
|
638
|
-
const [lightbox,
|
|
767
|
+
const [lightbox, setLightboxState] = useState<{ src: string; alt: string; images?: LightboxImage[] } | null>(null);
|
|
768
|
+
// A host that handles image clicks (the Inbox) gets them; otherwise the lightbox opens, stepping through
|
|
769
|
+
// the document's images when the host asked for it.
|
|
770
|
+
const setLightbox = (image: { src: string; alt: string }) => {
|
|
771
|
+
if (hostImageClick) {
|
|
772
|
+
hostImageClick(image.src, image.alt);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
setLightboxState(imageLightboxStepping ? { ...image, images: collectDocumentImages(containerRef.current) } : image);
|
|
776
|
+
};
|
|
639
777
|
const [locationHash, setLocationHash] = useState(() => window.location.hash);
|
|
640
778
|
const globalCommentButtonRef = useRef<HTMLButtonElement>(null);
|
|
641
779
|
// Read through a ref: only the imperative removeHighlight path below needs
|
|
@@ -690,9 +828,9 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
690
828
|
// annotations that carry `questionAnswer` (matched by question key, so an
|
|
691
829
|
// answer follows its question across re-parses).
|
|
692
830
|
const questionIndex = useMemo(() => {
|
|
693
|
-
const list = indexQuestionBlocks(blocks);
|
|
831
|
+
const list = indexQuestionBlocks(blocks, { decisionLines: questionDecisionLines });
|
|
694
832
|
return { byBlock: new Map(list.map((q) => [q.blockId, q])), total: list.length };
|
|
695
|
-
}, [blocks]);
|
|
833
|
+
}, [blocks, questionDecisionLines]);
|
|
696
834
|
// A host that keeps answers itself (`questionAnswers`) replaces the
|
|
697
835
|
// annotation-derived map; each entry is validated like any other reader.
|
|
698
836
|
const answersByKey = useMemo(
|
|
@@ -722,8 +860,8 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
722
860
|
codeBlock: { block: Block; element: HTMLElement };
|
|
723
861
|
} | null>(null);
|
|
724
862
|
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
725
|
-
const readOnlyRef = useRef(
|
|
726
|
-
readOnlyRef.current =
|
|
863
|
+
const readOnlyRef = useRef(authoringOff);
|
|
864
|
+
readOnlyRef.current = authoringOff;
|
|
727
865
|
const stickySentinelRef = useRef<HTMLDivElement>(null);
|
|
728
866
|
const lastAutoScrolledHashRef = useRef<string | null>(null);
|
|
729
867
|
const [isStuck, setIsStuck] = useState(false);
|
|
@@ -755,6 +893,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
755
893
|
removeHighlight: hookRemoveHighlight,
|
|
756
894
|
clearAllHighlights,
|
|
757
895
|
applyAnnotations,
|
|
896
|
+
takeSelection,
|
|
758
897
|
} = useAnnotationHighlighter({
|
|
759
898
|
containerRef,
|
|
760
899
|
annotations,
|
|
@@ -762,7 +901,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
762
901
|
onSelectAnnotation,
|
|
763
902
|
selectedAnnotationId,
|
|
764
903
|
mode,
|
|
765
|
-
enabled:
|
|
904
|
+
enabled: selectionCommentsOn,
|
|
766
905
|
// Markdown documents drift between the moment a draft/share is written and
|
|
767
906
|
// the moment it is restored (a plan revision, a re-rendered block, a
|
|
768
907
|
// renderer change that adds or drops elements — #1509 hoisted an alert's
|
|
@@ -774,6 +913,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
774
913
|
verifyRestoredContent: true,
|
|
775
914
|
onRestoreMismatch: handleRestoreMismatch,
|
|
776
915
|
onRestoreReport,
|
|
916
|
+
onHostDraft: hostComposes ? onHostDraft : undefined,
|
|
777
917
|
});
|
|
778
918
|
|
|
779
919
|
// Refs for code block annotation path
|
|
@@ -945,7 +1085,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
945
1085
|
});
|
|
946
1086
|
}, [applyCodeBlockAnnotation, blocks]);
|
|
947
1087
|
|
|
948
|
-
const vimModeActive = vimModeEnabled && !
|
|
1088
|
+
const vimModeActive = vimModeEnabled && !authoringOff;
|
|
949
1089
|
const keyboardCodeBlockToolbarOpen = codeBlockToolbar?.activation === 'keyboard';
|
|
950
1090
|
const vimBlocked = !!toolbarState
|
|
951
1091
|
|| !!hookCommentPopover
|
|
@@ -974,12 +1114,16 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
974
1114
|
onHandledCommand: handleVimCommand,
|
|
975
1115
|
});
|
|
976
1116
|
|
|
1117
|
+
// With the composer handed to the host, a pinpoint tap opens it at once.
|
|
1118
|
+
const hostPinpointRange = useCallback((range: Range) => highlightRange(range, 'comment'), [highlightRange]);
|
|
1119
|
+
const noHostCodeBlockClick = useCallback(() => {}, []);
|
|
977
1120
|
const { hoverTarget, clearHover: clearPinpointHover } = usePinpoint({
|
|
978
1121
|
containerRef,
|
|
979
1122
|
inputMethod,
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1123
|
+
// A host composer's draft stays open while the host composes; the next tap replaces it.
|
|
1124
|
+
enabled: !authoringOff && (hostComposes || (!toolbarState && !hookCommentPopover)) && !viewerCommentPopover && !hookQuickLabelPicker && !codeBlockQuickLabelPicker && !(isPlanDiffActive ?? false) && !vim.helpOpen,
|
|
1125
|
+
onSelectRange: hostComposes ? hostPinpointRange : highlightRange,
|
|
1126
|
+
onCodeBlockClick: hostComposes ? noHostCodeBlockClick : handlePinpointCodeBlockClick,
|
|
983
1127
|
});
|
|
984
1128
|
clearPinpointHoverRef.current = clearPinpointHover;
|
|
985
1129
|
const vimOwnsHudTarget = vimHudEnabled
|
|
@@ -998,7 +1142,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
998
1142
|
: (inputMethod === 'pinpoint' ? hoverTarget : null) ?? legacyVimTarget;
|
|
999
1143
|
|
|
1000
1144
|
useEffect(() => {
|
|
1001
|
-
if (!
|
|
1145
|
+
if (!authoringOff) return;
|
|
1002
1146
|
if (hoverTimeoutRef.current) {
|
|
1003
1147
|
clearTimeout(hoverTimeoutRef.current);
|
|
1004
1148
|
hoverTimeoutRef.current = null;
|
|
@@ -1007,7 +1151,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1007
1151
|
setIsCodeBlockToolbarExiting(false);
|
|
1008
1152
|
setViewerCommentPopover(null);
|
|
1009
1153
|
setCodeBlockQuickLabelPicker(null);
|
|
1010
|
-
}, [
|
|
1154
|
+
}, [authoringOff]);
|
|
1011
1155
|
|
|
1012
1156
|
useEffect(() => {
|
|
1013
1157
|
if (!vimOwnsDocumentNavigation) return;
|
|
@@ -1153,7 +1297,8 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1153
1297
|
},
|
|
1154
1298
|
clearAllHighlights,
|
|
1155
1299
|
applySharedAnnotations: applyAnnotations,
|
|
1156
|
-
|
|
1300
|
+
takeSelection,
|
|
1301
|
+
}), [hookRemoveHighlight, clearAllHighlights, applyAnnotations, blocks, takeSelection]);
|
|
1157
1302
|
|
|
1158
1303
|
// --- Viewer-specific: code block annotation ---
|
|
1159
1304
|
|
|
@@ -1266,7 +1411,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1266
1411
|
</button>
|
|
1267
1412
|
)}
|
|
1268
1413
|
|
|
1269
|
-
{!
|
|
1414
|
+
{!authoringOff && onAddGlobalAttachment && onRemoveGlobalAttachment && (
|
|
1270
1415
|
<AttachmentsButton
|
|
1271
1416
|
images={globalAttachments}
|
|
1272
1417
|
onAdd={onAddGlobalAttachment}
|
|
@@ -1276,7 +1421,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1276
1421
|
/>
|
|
1277
1422
|
)}
|
|
1278
1423
|
|
|
1279
|
-
{!
|
|
1424
|
+
{!authoringOff && (
|
|
1280
1425
|
<button
|
|
1281
1426
|
ref={globalCommentButtonRef}
|
|
1282
1427
|
onClick={() => {
|
|
@@ -1348,6 +1493,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1348
1493
|
|
|
1349
1494
|
return (
|
|
1350
1495
|
<CodePathValidationContext.Provider value={codePathValidation}>
|
|
1496
|
+
<LocalLinkContext.Provider value={resolveLocalLink ?? null}>
|
|
1351
1497
|
<DiagramAnchorClaimsContext.Provider value={diagramClaims}>
|
|
1352
1498
|
<div className="relative z-50 w-full" style={maxWidth === null ? undefined : { maxWidth: maxWidth ?? 832 }}>
|
|
1353
1499
|
{taterMode && <TaterSpriteSitting />}
|
|
@@ -1419,7 +1565,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1419
1565
|
<>
|
|
1420
1566
|
{badgeClearance > 0 && <div data-print-hide style={{ height: badgeClearance }} aria-hidden="true" />}
|
|
1421
1567
|
{stickyActions && <div ref={stickySentinelRef} className="h-0 w-0 float-right" aria-hidden="true" />}
|
|
1422
|
-
{stickyActions ? (
|
|
1568
|
+
{answerOnly || hostComposes ? null : stickyActions ? (
|
|
1423
1569
|
<StickyActionsLane className={`flex items-start gap-1 md:gap-2 rounded-lg p-1 md:p-2 transition-colors duration-150 ${isStuck ? 'bg-card/95 backdrop-blur-sm shadow-sm' : ''} ${gridEnabled ? '-mr-3 md:-mr-5 lg:-mr-7 xl:-mr-9' : '-mr-1 md:-mr-2'} mt-6 md:-mt-5 lg:-mt-7 xl:-mt-9`}>
|
|
1424
1570
|
{documentActions}
|
|
1425
1571
|
</StickyActionsLane>
|
|
@@ -1449,8 +1595,8 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1449
1595
|
onOpenCodeFile={onOpenCodeFile}
|
|
1450
1596
|
onToggleCheckbox={readOnly ? undefined : onToggleCheckbox}
|
|
1451
1597
|
checkboxOverrides={checkboxOverrides}
|
|
1452
|
-
githubRepo={
|
|
1453
|
-
repoHost={
|
|
1598
|
+
githubRepo={linkRepo?.display}
|
|
1599
|
+
repoHost={linkRepo?.host}
|
|
1454
1600
|
headingAnchorId={headingSlugMap.get(block.id)}
|
|
1455
1601
|
onNavigateAnchor={scrollToAnchor}
|
|
1456
1602
|
/>
|
|
@@ -1465,10 +1611,10 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1465
1611
|
annotations={annotations}
|
|
1466
1612
|
selectedAnnotationId={selectedAnnotationId}
|
|
1467
1613
|
onSelectAnnotation={onSelectAnnotation}
|
|
1468
|
-
onAddAnnotation={
|
|
1469
|
-
readOnly={
|
|
1614
|
+
onAddAnnotation={authoringOff ? undefined : onAddAnnotation}
|
|
1615
|
+
readOnly={authoringOff}
|
|
1470
1616
|
onRestoreReport={onRestoreReport}
|
|
1471
|
-
onAskAI={
|
|
1617
|
+
onAskAI={authoringOff ? undefined : onAskAI}
|
|
1472
1618
|
askAISourcePath={linkedDocInfo?.filepath ?? sourceInfo}
|
|
1473
1619
|
/>
|
|
1474
1620
|
</Suspense>
|
|
@@ -1479,10 +1625,10 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1479
1625
|
annotations={annotations}
|
|
1480
1626
|
selectedAnnotationId={selectedAnnotationId}
|
|
1481
1627
|
onSelectAnnotation={onSelectAnnotation}
|
|
1482
|
-
onAddAnnotation={
|
|
1483
|
-
readOnly={
|
|
1628
|
+
onAddAnnotation={authoringOff ? undefined : onAddAnnotation}
|
|
1629
|
+
readOnly={authoringOff}
|
|
1484
1630
|
onRestoreReport={onRestoreReport}
|
|
1485
|
-
onAskAI={
|
|
1631
|
+
onAskAI={authoringOff ? undefined : onAskAI}
|
|
1486
1632
|
askAISourcePath={linkedDocInfo?.filepath ?? sourceInfo}
|
|
1487
1633
|
/>
|
|
1488
1634
|
</Suspense>
|
|
@@ -1494,8 +1640,8 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1494
1640
|
onImageClick={(src, alt) => setLightbox({ src, alt })}
|
|
1495
1641
|
onOpenLinkedDoc={onOpenLinkedDoc}
|
|
1496
1642
|
onOpenCodeFile={onOpenCodeFile}
|
|
1497
|
-
githubRepo={
|
|
1498
|
-
repoHost={
|
|
1643
|
+
githubRepo={linkRepo?.display}
|
|
1644
|
+
repoHost={linkRepo?.host}
|
|
1499
1645
|
onNavigateAnchor={scrollToAnchor}
|
|
1500
1646
|
onHover={(element) => {
|
|
1501
1647
|
if (tableHoverTimeoutRef.current) {
|
|
@@ -1503,7 +1649,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1503
1649
|
tableHoverTimeoutRef.current = null;
|
|
1504
1650
|
}
|
|
1505
1651
|
setIsTableToolbarExiting(false);
|
|
1506
|
-
if (!toolbarState) {
|
|
1652
|
+
if (!toolbarState && !hostComposes) {
|
|
1507
1653
|
setHoveredTable({ block: group.block, element });
|
|
1508
1654
|
}
|
|
1509
1655
|
}}
|
|
@@ -1521,7 +1667,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1521
1667
|
<CodeBlock
|
|
1522
1668
|
key={group.block.id}
|
|
1523
1669
|
block={group.block}
|
|
1524
|
-
onHover={
|
|
1670
|
+
onHover={authoringOff || hostComposes || inputMethod === 'pinpoint' ? undefined : (element) => {
|
|
1525
1671
|
// Clear any pending leave timeout
|
|
1526
1672
|
if (hoverTimeoutRef.current) {
|
|
1527
1673
|
clearTimeout(hoverTimeoutRef.current);
|
|
@@ -1542,7 +1688,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1542
1688
|
});
|
|
1543
1689
|
}
|
|
1544
1690
|
}}
|
|
1545
|
-
onLeave={
|
|
1691
|
+
onLeave={authoringOff || hostComposes || inputMethod === 'pinpoint' ? undefined : () => {
|
|
1546
1692
|
if (keyboardCodeBlockToolbarOpen) return;
|
|
1547
1693
|
// Delay then start exit animation
|
|
1548
1694
|
hoverTimeoutRef.current = setTimeout(() => {
|
|
@@ -1555,7 +1701,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1555
1701
|
}, 100);
|
|
1556
1702
|
}}
|
|
1557
1703
|
isHovered={
|
|
1558
|
-
!
|
|
1704
|
+
!authoringOff
|
|
1559
1705
|
&& inputMethod !== 'pinpoint'
|
|
1560
1706
|
&& !vimOwnsDocumentNavigation
|
|
1561
1707
|
&& codeBlockToolbar?.block.id === group.block.id
|
|
@@ -1564,7 +1710,17 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1564
1710
|
) : (
|
|
1565
1711
|
(() => {
|
|
1566
1712
|
const question = questionIndex.byBlock.get(group.block.id);
|
|
1567
|
-
|
|
1713
|
+
const hostCard = question && renderQuestionCard
|
|
1714
|
+
? renderQuestionCard(question, answersByKey.get(question.question.key))
|
|
1715
|
+
: undefined;
|
|
1716
|
+
if (hostCard !== undefined) {
|
|
1717
|
+
return (
|
|
1718
|
+
<div key={group.block.id} data-block-id={group.block.id} data-block-type="directive" className={textCommentsOnly ? 'annotation-exclude' : undefined}>
|
|
1719
|
+
{hostCard}
|
|
1720
|
+
</div>
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
const rendered = (
|
|
1568
1724
|
<BlockRenderer
|
|
1569
1725
|
imageBaseDir={imageBaseDir}
|
|
1570
1726
|
onImageClick={(src, alt) => setLightbox({ src, alt })}
|
|
@@ -1575,33 +1731,47 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1575
1731
|
onNavigateAnchor={scrollToAnchor}
|
|
1576
1732
|
onToggleCheckbox={readOnly ? undefined : onToggleCheckbox}
|
|
1577
1733
|
checkboxOverrides={checkboxOverrides}
|
|
1578
|
-
githubRepo={
|
|
1579
|
-
repoHost={
|
|
1734
|
+
githubRepo={linkRepo?.display}
|
|
1735
|
+
repoHost={linkRepo?.host}
|
|
1580
1736
|
headingAnchorId={headingSlugMap.get(group.block.id)}
|
|
1581
1737
|
question={question}
|
|
1582
1738
|
questionTotal={questionIndex.total}
|
|
1583
1739
|
questionAnswer={question ? answersByKey.get(question.question.key) : undefined}
|
|
1584
|
-
onAnswerQuestion={readOnly ? undefined : onAnswerQuestion}
|
|
1585
|
-
onSaveQuestionAnswer={readOnly ? undefined : onSaveQuestionAnswer}
|
|
1740
|
+
onAnswerQuestion={readOnly || (question && questionReadOnly?.(question)) ? undefined : onAnswerQuestion}
|
|
1741
|
+
onSaveQuestionAnswer={readOnly || (question && questionReadOnly?.(question)) ? undefined : onSaveQuestionAnswer}
|
|
1586
1742
|
saveQuestionAnswerLabel={saveQuestionAnswerLabel}
|
|
1587
1743
|
renderQuestionFooter={renderQuestionFooter}
|
|
1588
1744
|
questionDecisionRecording={question ? questionDecisionRecording?.(question) : undefined}
|
|
1589
1745
|
questionDecisionScope={questionDecisionScope}
|
|
1590
|
-
onToggleQuestionDecisionRecording={readOnly ? undefined : onToggleQuestionDecisionRecording}
|
|
1591
|
-
onOpenQuestionDecision={readOnly ? undefined : onOpenQuestionDecision}
|
|
1746
|
+
onToggleQuestionDecisionRecording={readOnly || (question && (questionDecisionLocked?.(question) || questionReadOnly?.(question))) ? undefined : onToggleQuestionDecisionRecording}
|
|
1747
|
+
onOpenQuestionDecision={readOnly || (question && (questionDecisionLocked?.(question) || questionReadOnly?.(question))) ? undefined : onOpenQuestionDecision}
|
|
1592
1748
|
questionStatusTag={questionStatusTag}
|
|
1749
|
+
questionStatusLabel={questionStatusLabel}
|
|
1750
|
+
renderQuestionBelowPrompt={renderQuestionBelowPrompt}
|
|
1593
1751
|
/>
|
|
1594
1752
|
);
|
|
1753
|
+
// `textComments`: a question card's words are not commented on
|
|
1754
|
+
// (its own controls already are excluded); a box-less wrapper
|
|
1755
|
+
// keeps its layout.
|
|
1756
|
+
if (question && textCommentsOnly) {
|
|
1757
|
+
return (
|
|
1758
|
+
<div key={group.block.id} className="annotation-exclude" style={{ display: 'contents' }}>
|
|
1759
|
+
{rendered}
|
|
1760
|
+
</div>
|
|
1761
|
+
);
|
|
1762
|
+
}
|
|
1763
|
+
return rendered;
|
|
1595
1764
|
})()
|
|
1596
1765
|
)
|
|
1597
1766
|
)}
|
|
1598
1767
|
|
|
1599
1768
|
{/* Text selection toolbar */}
|
|
1600
|
-
{!
|
|
1769
|
+
{selectionCommentsOn && !hostComposes && toolbarState && (
|
|
1601
1770
|
<ToolbarErrorBoundary>
|
|
1602
1771
|
<AnnotationToolbar
|
|
1603
1772
|
element={toolbarState.element}
|
|
1604
1773
|
positionMode="center-above"
|
|
1774
|
+
commentOnly={textCommentsOnly || undefined}
|
|
1605
1775
|
onAnnotate={handleAnnotate}
|
|
1606
1776
|
onClose={handleToolbarClose}
|
|
1607
1777
|
onRequestComment={handleRequestComment}
|
|
@@ -1651,7 +1821,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1651
1821
|
)}
|
|
1652
1822
|
|
|
1653
1823
|
{/* Code block hover toolbar */}
|
|
1654
|
-
{!
|
|
1824
|
+
{!authoringOff
|
|
1655
1825
|
&& codeBlockToolbar
|
|
1656
1826
|
&& !toolbarState
|
|
1657
1827
|
&& !(vimOwnsDocumentNavigation && codeBlockToolbar.activation === 'pointer')
|
|
@@ -1701,8 +1871,8 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1701
1871
|
onImageClick={(src, alt) => setLightbox({ src, alt })}
|
|
1702
1872
|
onOpenLinkedDoc={onOpenLinkedDoc}
|
|
1703
1873
|
onOpenCodeFile={onOpenCodeFile}
|
|
1704
|
-
githubRepo={
|
|
1705
|
-
repoHost={
|
|
1874
|
+
githubRepo={linkRepo?.display}
|
|
1875
|
+
repoHost={linkRepo?.host}
|
|
1706
1876
|
onNavigateAnchor={scrollToAnchor}
|
|
1707
1877
|
/>
|
|
1708
1878
|
)}
|
|
@@ -1736,7 +1906,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1736
1906
|
)}
|
|
1737
1907
|
|
|
1738
1908
|
{/* Comment popover — hook handles text selection, Viewer handles global + code block */}
|
|
1739
|
-
{!
|
|
1909
|
+
{selectionCommentsOn && !hostComposes && hookCommentPopover && (
|
|
1740
1910
|
<CommentPopover
|
|
1741
1911
|
anchorEl={hookCommentPopover.anchorEl}
|
|
1742
1912
|
contextText={hookCommentPopover.contextText}
|
|
@@ -1758,7 +1928,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1758
1928
|
}}
|
|
1759
1929
|
/>
|
|
1760
1930
|
)}
|
|
1761
|
-
{!
|
|
1931
|
+
{!authoringOff && viewerCommentPopover && (
|
|
1762
1932
|
<CommentPopover
|
|
1763
1933
|
anchorEl={viewerCommentPopover.anchorEl}
|
|
1764
1934
|
contextText={viewerCommentPopover.contextText}
|
|
@@ -1788,7 +1958,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1788
1958
|
)}
|
|
1789
1959
|
|
|
1790
1960
|
{/* Quick Label floating picker — hook handles text selection, Viewer handles code blocks */}
|
|
1791
|
-
{!
|
|
1961
|
+
{selectionCommentsOn && !hostComposes && hookQuickLabelPicker && (
|
|
1792
1962
|
<FloatingQuickLabelPicker
|
|
1793
1963
|
anchorEl={hookQuickLabelPicker.anchorEl}
|
|
1794
1964
|
cursorHint={hookQuickLabelPicker.cursorHint}
|
|
@@ -1796,7 +1966,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1796
1966
|
onDismiss={hookQuickLabelPickerDismiss}
|
|
1797
1967
|
/>
|
|
1798
1968
|
)}
|
|
1799
|
-
{!
|
|
1969
|
+
{!authoringOff && codeBlockQuickLabelPicker && (
|
|
1800
1970
|
<FloatingQuickLabelPicker
|
|
1801
1971
|
anchorEl={codeBlockQuickLabelPicker.anchorEl}
|
|
1802
1972
|
onSelect={(label: QuickLabel) => {
|
|
@@ -1820,11 +1990,12 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1820
1990
|
|
|
1821
1991
|
{/* Image lightbox */}
|
|
1822
1992
|
{lightbox && createPortal(
|
|
1823
|
-
<ImageLightbox src={lightbox.src} alt={lightbox.alt} onClose={() =>
|
|
1993
|
+
<ImageLightbox src={lightbox.src} alt={lightbox.alt} images={lightbox.images} onClose={() => setLightboxState(null)} />,
|
|
1824
1994
|
document.body
|
|
1825
1995
|
)}
|
|
1826
1996
|
</div>
|
|
1827
1997
|
</DiagramAnchorClaimsContext.Provider>
|
|
1998
|
+
</LocalLinkContext.Provider>
|
|
1828
1999
|
</CodePathValidationContext.Provider>
|
|
1829
2000
|
);
|
|
1830
2001
|
});
|
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { getProviderMeta } from '../ProviderIcons';
|
|
3
3
|
import { findSessionBridge, type AIProviderOption } from '../../utils/aiProvider';
|
|
4
4
|
import { ModelSourceHint, modelSourceToolForProvider } from '../ModelSourceHint';
|
|
5
|
+
import { AskSessionSetupHint } from './AskSessionSetupHint';
|
|
5
6
|
|
|
6
7
|
interface AIProviderBarProps {
|
|
7
8
|
providers: AIProviderOption[];
|
|
@@ -11,6 +12,11 @@ interface AIProviderBarProps {
|
|
|
11
12
|
onProviderChange: (providerId: string) => void;
|
|
12
13
|
onModelChange: (model: string) => void;
|
|
13
14
|
onReasoningEffortChange?: (effort: string | null) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Optional muted line under the bar saying how to get "Ask this session"
|
|
17
|
+
* (see askSessionSetupHint). Absent: the bar renders exactly as before.
|
|
18
|
+
*/
|
|
19
|
+
setupHint?: string | null;
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
export const AIProviderBar: React.FC<AIProviderBarProps> = ({
|
|
@@ -21,12 +27,16 @@ export const AIProviderBar: React.FC<AIProviderBarProps> = ({
|
|
|
21
27
|
onProviderChange,
|
|
22
28
|
onModelChange,
|
|
23
29
|
onReasoningEffortChange,
|
|
30
|
+
setupHint,
|
|
24
31
|
}) => {
|
|
25
32
|
if (providers.length === 0) {
|
|
26
33
|
return (
|
|
34
|
+
<>
|
|
27
35
|
<div className="border-t border-border/50 px-2 py-1.5 text-[11px] text-muted-foreground/50">
|
|
28
36
|
No AI providers available
|
|
29
37
|
</div>
|
|
38
|
+
<AskSessionSetupHint text={setupHint} />
|
|
39
|
+
</>
|
|
30
40
|
);
|
|
31
41
|
}
|
|
32
42
|
|
|
@@ -115,6 +125,7 @@ export const AIProviderBar: React.FC<AIProviderBarProps> = ({
|
|
|
115
125
|
info={currentProvider}
|
|
116
126
|
className="-mt-1 px-2 pb-1.5 text-[10px] text-muted-foreground/50"
|
|
117
127
|
/>
|
|
128
|
+
<AskSessionSetupHint text={setupHint} />
|
|
118
129
|
</>
|
|
119
130
|
);
|
|
120
131
|
};
|