@plannotator/ui 0.52.1 → 0.53.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.
- package/HANDOFF.md +231 -27
- package/README.md +29 -4
- package/components/AnnotationPanel.tsx +159 -107
- package/components/AnnotationToolbar.tsx +17 -14
- package/components/AnnotationToolstrip.tsx +9 -0
- package/components/BlockRenderer.tsx +20 -14
- package/components/CodeFilePopout.tsx +131 -68
- package/components/CommentPopover.tsx +6 -349
- package/components/ComposerTokenTextarea.tsx +379 -0
- package/components/ExportModal.tsx +6 -1
- package/components/HostTitle.tsx +108 -0
- 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/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 +28 -6
- package/components/Tooltip.tsx +51 -4
- package/components/Viewer.tsx +189 -48
- 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/diagram/DiagramViewer.tsx +41 -4
- package/components/diagram/index.ts +1 -1
- package/components/html-viewer/HtmlViewer.tsx +73 -3
- package/components/html-viewer/bridge-script.asset.js +86 -0
- package/components/html-viewer/bridge-script.ts +86 -0
- 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/useOpenInApps.ts +88 -0
- package/hooks/useUpdateCheck.ts +38 -98
- package/hooks/useUpdateCheckSetting.ts +43 -0
- package/hooks/useUpdateToast.ts +101 -0
- package/package.json +2 -2
- package/shortcuts/code-review/markdownPreview.shortcuts.ts +18 -0
- package/shortcuts/index.ts +1 -0
- 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/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,16 @@ 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;
|
|
260
329
|
/** Opt-in Vim-style keyboard selection. Default false for compatibility. */
|
|
261
330
|
vimModeEnabled?: boolean;
|
|
262
331
|
/** Replace the compact Vim badge with the live video-style key HUD. */
|
|
@@ -268,12 +337,36 @@ export interface ViewerProps {
|
|
|
268
337
|
/** Fires once per highlight-restore pass with what it tried and what it could
|
|
269
338
|
* not anchor, so a host can mark the leftovers in its annotation panel. */
|
|
270
339
|
onRestoreReport?: (report: AnnotationRestoreReport) => void;
|
|
340
|
+
/**
|
|
341
|
+
* Opt-in host capability: the composer handed to the host (see
|
|
342
|
+
* `HostDraft`). A selection settling, a pinpoint tap or Comment mode reports
|
|
343
|
+
* a draft here instead of opening the toolbar or a composer (a pinpoint tap
|
|
344
|
+
* reports `intent: 'compose'`), and null when it closes. The document
|
|
345
|
+
* actions (Global comment, Copy) and the code-block and table hover
|
|
346
|
+
* toolbars are not drawn, and a pinpoint tap on a code block does nothing.
|
|
347
|
+
* The host saves the comment and draws it through `annotations` /
|
|
348
|
+
* `applySharedAnnotations`. Ignored when authoring is off. Absent →
|
|
349
|
+
* unchanged.
|
|
350
|
+
*/
|
|
351
|
+
onHostDraft?: (draft: HostDraft | null) => void;
|
|
271
352
|
}
|
|
272
353
|
|
|
273
354
|
export interface ViewerHandle {
|
|
274
355
|
removeHighlight: (id: string) => void;
|
|
275
356
|
clearAllHighlights: () => void;
|
|
276
357
|
applySharedAnnotations: (annotations: Annotation[]) => void;
|
|
358
|
+
/** `HtmlViewer` only: move a pinned draft to the element around it
|
|
359
|
+
* (`'parent'`) or back in (`'child'`), for a host that composes
|
|
360
|
+
* (`onHostDraft`). The markdown `Viewer` does not implement it. */
|
|
361
|
+
stepPin?: (direction: 'parent' | 'child') => void;
|
|
362
|
+
/** `HtmlViewer` only: scroll the pinned draft into the middle of the
|
|
363
|
+
* frame, for a host whose panel covers part of it. */
|
|
364
|
+
revealPin?: () => void;
|
|
365
|
+
/** Markdown `Viewer` only, for a host that composes (`onHostDraft`) on a
|
|
366
|
+
* touch screen: make the page's text selection a draft now (the host's own
|
|
367
|
+
* Comment action), reported through `onHostDraft` as `'selection'`.
|
|
368
|
+
* Returns false when nothing in the document is selected. */
|
|
369
|
+
takeSelection?: () => boolean;
|
|
277
370
|
}
|
|
278
371
|
|
|
279
372
|
interface CodeBlockToolbarTarget {
|
|
@@ -588,6 +681,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
588
681
|
onAddGlobalAttachment,
|
|
589
682
|
onRemoveGlobalAttachment,
|
|
590
683
|
repoInfo,
|
|
684
|
+
refRepo,
|
|
591
685
|
stickyActions = true,
|
|
592
686
|
annotationHeader,
|
|
593
687
|
gridEnabled = false,
|
|
@@ -601,8 +695,11 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
601
695
|
maxWidth,
|
|
602
696
|
onOpenLinkedDoc,
|
|
603
697
|
onOpenCodeFile,
|
|
698
|
+
resolveLocalLink,
|
|
604
699
|
linkedDocInfo,
|
|
605
700
|
imageBaseDir,
|
|
701
|
+
onImageClick: hostImageClick,
|
|
702
|
+
imageLightboxStepping = false,
|
|
606
703
|
codePathBaseDir,
|
|
607
704
|
disableCodePathValidation,
|
|
608
705
|
onRestoreReport,
|
|
@@ -621,21 +718,44 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
621
718
|
renderQuestionFooter,
|
|
622
719
|
questionDecisionRecording,
|
|
623
720
|
questionDecisionScope,
|
|
721
|
+
questionDecisionLines = true,
|
|
624
722
|
onToggleQuestionDecisionRecording,
|
|
625
723
|
onOpenQuestionDecision,
|
|
724
|
+
questionDecisionLocked,
|
|
725
|
+
questionReadOnly,
|
|
626
726
|
questionStatusTag,
|
|
727
|
+
questionStatusLabel,
|
|
728
|
+
renderQuestionCard,
|
|
729
|
+
renderQuestionBelowPrompt,
|
|
627
730
|
onAskAI,
|
|
628
731
|
allowImages = true,
|
|
629
732
|
readOnly = false,
|
|
733
|
+
answerOnly = false,
|
|
630
734
|
vimModeEnabled = false,
|
|
631
735
|
vimHudEnabled = false,
|
|
632
736
|
vimHudKeyPanelEnabled = true,
|
|
633
737
|
onVimHudKeyPanelChange,
|
|
738
|
+
onHostDraft,
|
|
634
739
|
}, ref) => {
|
|
635
|
-
|
|
740
|
+
// Annotation authoring is off when read-only, and when the host only wants
|
|
741
|
+
// answers (`answerOnly`); question cards and checkboxes follow `readOnly`.
|
|
742
|
+
const authoringOff = readOnly || answerOnly;
|
|
743
|
+
const hostComposes = onHostDraft !== undefined && !authoringOff;
|
|
744
|
+
const viewerAnnotationHeader = authoringOff ? undefined : annotationHeader;
|
|
636
745
|
const hasViewerAnnotationHeader = viewerAnnotationHeader !== undefined;
|
|
746
|
+
// Where `#123` / `@user` link: `refRepo` when the host gave one, else `repoInfo`.
|
|
747
|
+
const linkRepo = refRepo ?? repoInfo;
|
|
637
748
|
const [copied, setCopied] = useState(false);
|
|
638
|
-
const [lightbox,
|
|
749
|
+
const [lightbox, setLightboxState] = useState<{ src: string; alt: string; images?: LightboxImage[] } | null>(null);
|
|
750
|
+
// A host that handles image clicks (the Inbox) gets them; otherwise the lightbox opens, stepping through
|
|
751
|
+
// the document's images when the host asked for it.
|
|
752
|
+
const setLightbox = (image: { src: string; alt: string }) => {
|
|
753
|
+
if (hostImageClick) {
|
|
754
|
+
hostImageClick(image.src, image.alt);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
setLightboxState(imageLightboxStepping ? { ...image, images: collectDocumentImages(containerRef.current) } : image);
|
|
758
|
+
};
|
|
639
759
|
const [locationHash, setLocationHash] = useState(() => window.location.hash);
|
|
640
760
|
const globalCommentButtonRef = useRef<HTMLButtonElement>(null);
|
|
641
761
|
// Read through a ref: only the imperative removeHighlight path below needs
|
|
@@ -690,9 +810,9 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
690
810
|
// annotations that carry `questionAnswer` (matched by question key, so an
|
|
691
811
|
// answer follows its question across re-parses).
|
|
692
812
|
const questionIndex = useMemo(() => {
|
|
693
|
-
const list = indexQuestionBlocks(blocks);
|
|
813
|
+
const list = indexQuestionBlocks(blocks, { decisionLines: questionDecisionLines });
|
|
694
814
|
return { byBlock: new Map(list.map((q) => [q.blockId, q])), total: list.length };
|
|
695
|
-
}, [blocks]);
|
|
815
|
+
}, [blocks, questionDecisionLines]);
|
|
696
816
|
// A host that keeps answers itself (`questionAnswers`) replaces the
|
|
697
817
|
// annotation-derived map; each entry is validated like any other reader.
|
|
698
818
|
const answersByKey = useMemo(
|
|
@@ -722,8 +842,8 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
722
842
|
codeBlock: { block: Block; element: HTMLElement };
|
|
723
843
|
} | null>(null);
|
|
724
844
|
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
725
|
-
const readOnlyRef = useRef(
|
|
726
|
-
readOnlyRef.current =
|
|
845
|
+
const readOnlyRef = useRef(authoringOff);
|
|
846
|
+
readOnlyRef.current = authoringOff;
|
|
727
847
|
const stickySentinelRef = useRef<HTMLDivElement>(null);
|
|
728
848
|
const lastAutoScrolledHashRef = useRef<string | null>(null);
|
|
729
849
|
const [isStuck, setIsStuck] = useState(false);
|
|
@@ -755,6 +875,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
755
875
|
removeHighlight: hookRemoveHighlight,
|
|
756
876
|
clearAllHighlights,
|
|
757
877
|
applyAnnotations,
|
|
878
|
+
takeSelection,
|
|
758
879
|
} = useAnnotationHighlighter({
|
|
759
880
|
containerRef,
|
|
760
881
|
annotations,
|
|
@@ -762,7 +883,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
762
883
|
onSelectAnnotation,
|
|
763
884
|
selectedAnnotationId,
|
|
764
885
|
mode,
|
|
765
|
-
enabled: !
|
|
886
|
+
enabled: !authoringOff,
|
|
766
887
|
// Markdown documents drift between the moment a draft/share is written and
|
|
767
888
|
// the moment it is restored (a plan revision, a re-rendered block, a
|
|
768
889
|
// renderer change that adds or drops elements — #1509 hoisted an alert's
|
|
@@ -774,6 +895,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
774
895
|
verifyRestoredContent: true,
|
|
775
896
|
onRestoreMismatch: handleRestoreMismatch,
|
|
776
897
|
onRestoreReport,
|
|
898
|
+
onHostDraft: hostComposes ? onHostDraft : undefined,
|
|
777
899
|
});
|
|
778
900
|
|
|
779
901
|
// Refs for code block annotation path
|
|
@@ -945,7 +1067,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
945
1067
|
});
|
|
946
1068
|
}, [applyCodeBlockAnnotation, blocks]);
|
|
947
1069
|
|
|
948
|
-
const vimModeActive = vimModeEnabled && !
|
|
1070
|
+
const vimModeActive = vimModeEnabled && !authoringOff;
|
|
949
1071
|
const keyboardCodeBlockToolbarOpen = codeBlockToolbar?.activation === 'keyboard';
|
|
950
1072
|
const vimBlocked = !!toolbarState
|
|
951
1073
|
|| !!hookCommentPopover
|
|
@@ -974,12 +1096,16 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
974
1096
|
onHandledCommand: handleVimCommand,
|
|
975
1097
|
});
|
|
976
1098
|
|
|
1099
|
+
// With the composer handed to the host, a pinpoint tap opens it at once.
|
|
1100
|
+
const hostPinpointRange = useCallback((range: Range) => highlightRange(range, 'comment'), [highlightRange]);
|
|
1101
|
+
const noHostCodeBlockClick = useCallback(() => {}, []);
|
|
977
1102
|
const { hoverTarget, clearHover: clearPinpointHover } = usePinpoint({
|
|
978
1103
|
containerRef,
|
|
979
1104
|
inputMethod,
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1105
|
+
// A host composer's draft stays open while the host composes; the next tap replaces it.
|
|
1106
|
+
enabled: !authoringOff && (hostComposes || (!toolbarState && !hookCommentPopover)) && !viewerCommentPopover && !hookQuickLabelPicker && !codeBlockQuickLabelPicker && !(isPlanDiffActive ?? false) && !vim.helpOpen,
|
|
1107
|
+
onSelectRange: hostComposes ? hostPinpointRange : highlightRange,
|
|
1108
|
+
onCodeBlockClick: hostComposes ? noHostCodeBlockClick : handlePinpointCodeBlockClick,
|
|
983
1109
|
});
|
|
984
1110
|
clearPinpointHoverRef.current = clearPinpointHover;
|
|
985
1111
|
const vimOwnsHudTarget = vimHudEnabled
|
|
@@ -998,7 +1124,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
998
1124
|
: (inputMethod === 'pinpoint' ? hoverTarget : null) ?? legacyVimTarget;
|
|
999
1125
|
|
|
1000
1126
|
useEffect(() => {
|
|
1001
|
-
if (!
|
|
1127
|
+
if (!authoringOff) return;
|
|
1002
1128
|
if (hoverTimeoutRef.current) {
|
|
1003
1129
|
clearTimeout(hoverTimeoutRef.current);
|
|
1004
1130
|
hoverTimeoutRef.current = null;
|
|
@@ -1007,7 +1133,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1007
1133
|
setIsCodeBlockToolbarExiting(false);
|
|
1008
1134
|
setViewerCommentPopover(null);
|
|
1009
1135
|
setCodeBlockQuickLabelPicker(null);
|
|
1010
|
-
}, [
|
|
1136
|
+
}, [authoringOff]);
|
|
1011
1137
|
|
|
1012
1138
|
useEffect(() => {
|
|
1013
1139
|
if (!vimOwnsDocumentNavigation) return;
|
|
@@ -1153,7 +1279,8 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1153
1279
|
},
|
|
1154
1280
|
clearAllHighlights,
|
|
1155
1281
|
applySharedAnnotations: applyAnnotations,
|
|
1156
|
-
|
|
1282
|
+
takeSelection,
|
|
1283
|
+
}), [hookRemoveHighlight, clearAllHighlights, applyAnnotations, blocks, takeSelection]);
|
|
1157
1284
|
|
|
1158
1285
|
// --- Viewer-specific: code block annotation ---
|
|
1159
1286
|
|
|
@@ -1266,7 +1393,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1266
1393
|
</button>
|
|
1267
1394
|
)}
|
|
1268
1395
|
|
|
1269
|
-
{!
|
|
1396
|
+
{!authoringOff && onAddGlobalAttachment && onRemoveGlobalAttachment && (
|
|
1270
1397
|
<AttachmentsButton
|
|
1271
1398
|
images={globalAttachments}
|
|
1272
1399
|
onAdd={onAddGlobalAttachment}
|
|
@@ -1276,7 +1403,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1276
1403
|
/>
|
|
1277
1404
|
)}
|
|
1278
1405
|
|
|
1279
|
-
{!
|
|
1406
|
+
{!authoringOff && (
|
|
1280
1407
|
<button
|
|
1281
1408
|
ref={globalCommentButtonRef}
|
|
1282
1409
|
onClick={() => {
|
|
@@ -1348,6 +1475,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1348
1475
|
|
|
1349
1476
|
return (
|
|
1350
1477
|
<CodePathValidationContext.Provider value={codePathValidation}>
|
|
1478
|
+
<LocalLinkContext.Provider value={resolveLocalLink ?? null}>
|
|
1351
1479
|
<DiagramAnchorClaimsContext.Provider value={diagramClaims}>
|
|
1352
1480
|
<div className="relative z-50 w-full" style={maxWidth === null ? undefined : { maxWidth: maxWidth ?? 832 }}>
|
|
1353
1481
|
{taterMode && <TaterSpriteSitting />}
|
|
@@ -1419,7 +1547,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1419
1547
|
<>
|
|
1420
1548
|
{badgeClearance > 0 && <div data-print-hide style={{ height: badgeClearance }} aria-hidden="true" />}
|
|
1421
1549
|
{stickyActions && <div ref={stickySentinelRef} className="h-0 w-0 float-right" aria-hidden="true" />}
|
|
1422
|
-
{stickyActions ? (
|
|
1550
|
+
{answerOnly || hostComposes ? null : stickyActions ? (
|
|
1423
1551
|
<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
1552
|
{documentActions}
|
|
1425
1553
|
</StickyActionsLane>
|
|
@@ -1449,8 +1577,8 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1449
1577
|
onOpenCodeFile={onOpenCodeFile}
|
|
1450
1578
|
onToggleCheckbox={readOnly ? undefined : onToggleCheckbox}
|
|
1451
1579
|
checkboxOverrides={checkboxOverrides}
|
|
1452
|
-
githubRepo={
|
|
1453
|
-
repoHost={
|
|
1580
|
+
githubRepo={linkRepo?.display}
|
|
1581
|
+
repoHost={linkRepo?.host}
|
|
1454
1582
|
headingAnchorId={headingSlugMap.get(block.id)}
|
|
1455
1583
|
onNavigateAnchor={scrollToAnchor}
|
|
1456
1584
|
/>
|
|
@@ -1465,10 +1593,10 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1465
1593
|
annotations={annotations}
|
|
1466
1594
|
selectedAnnotationId={selectedAnnotationId}
|
|
1467
1595
|
onSelectAnnotation={onSelectAnnotation}
|
|
1468
|
-
onAddAnnotation={
|
|
1469
|
-
readOnly={
|
|
1596
|
+
onAddAnnotation={authoringOff ? undefined : onAddAnnotation}
|
|
1597
|
+
readOnly={authoringOff}
|
|
1470
1598
|
onRestoreReport={onRestoreReport}
|
|
1471
|
-
onAskAI={
|
|
1599
|
+
onAskAI={authoringOff ? undefined : onAskAI}
|
|
1472
1600
|
askAISourcePath={linkedDocInfo?.filepath ?? sourceInfo}
|
|
1473
1601
|
/>
|
|
1474
1602
|
</Suspense>
|
|
@@ -1479,10 +1607,10 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1479
1607
|
annotations={annotations}
|
|
1480
1608
|
selectedAnnotationId={selectedAnnotationId}
|
|
1481
1609
|
onSelectAnnotation={onSelectAnnotation}
|
|
1482
|
-
onAddAnnotation={
|
|
1483
|
-
readOnly={
|
|
1610
|
+
onAddAnnotation={authoringOff ? undefined : onAddAnnotation}
|
|
1611
|
+
readOnly={authoringOff}
|
|
1484
1612
|
onRestoreReport={onRestoreReport}
|
|
1485
|
-
onAskAI={
|
|
1613
|
+
onAskAI={authoringOff ? undefined : onAskAI}
|
|
1486
1614
|
askAISourcePath={linkedDocInfo?.filepath ?? sourceInfo}
|
|
1487
1615
|
/>
|
|
1488
1616
|
</Suspense>
|
|
@@ -1494,8 +1622,8 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1494
1622
|
onImageClick={(src, alt) => setLightbox({ src, alt })}
|
|
1495
1623
|
onOpenLinkedDoc={onOpenLinkedDoc}
|
|
1496
1624
|
onOpenCodeFile={onOpenCodeFile}
|
|
1497
|
-
githubRepo={
|
|
1498
|
-
repoHost={
|
|
1625
|
+
githubRepo={linkRepo?.display}
|
|
1626
|
+
repoHost={linkRepo?.host}
|
|
1499
1627
|
onNavigateAnchor={scrollToAnchor}
|
|
1500
1628
|
onHover={(element) => {
|
|
1501
1629
|
if (tableHoverTimeoutRef.current) {
|
|
@@ -1503,7 +1631,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1503
1631
|
tableHoverTimeoutRef.current = null;
|
|
1504
1632
|
}
|
|
1505
1633
|
setIsTableToolbarExiting(false);
|
|
1506
|
-
if (!toolbarState) {
|
|
1634
|
+
if (!toolbarState && !hostComposes) {
|
|
1507
1635
|
setHoveredTable({ block: group.block, element });
|
|
1508
1636
|
}
|
|
1509
1637
|
}}
|
|
@@ -1521,7 +1649,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1521
1649
|
<CodeBlock
|
|
1522
1650
|
key={group.block.id}
|
|
1523
1651
|
block={group.block}
|
|
1524
|
-
onHover={
|
|
1652
|
+
onHover={authoringOff || hostComposes || inputMethod === 'pinpoint' ? undefined : (element) => {
|
|
1525
1653
|
// Clear any pending leave timeout
|
|
1526
1654
|
if (hoverTimeoutRef.current) {
|
|
1527
1655
|
clearTimeout(hoverTimeoutRef.current);
|
|
@@ -1542,7 +1670,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1542
1670
|
});
|
|
1543
1671
|
}
|
|
1544
1672
|
}}
|
|
1545
|
-
onLeave={
|
|
1673
|
+
onLeave={authoringOff || hostComposes || inputMethod === 'pinpoint' ? undefined : () => {
|
|
1546
1674
|
if (keyboardCodeBlockToolbarOpen) return;
|
|
1547
1675
|
// Delay then start exit animation
|
|
1548
1676
|
hoverTimeoutRef.current = setTimeout(() => {
|
|
@@ -1555,7 +1683,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1555
1683
|
}, 100);
|
|
1556
1684
|
}}
|
|
1557
1685
|
isHovered={
|
|
1558
|
-
!
|
|
1686
|
+
!authoringOff
|
|
1559
1687
|
&& inputMethod !== 'pinpoint'
|
|
1560
1688
|
&& !vimOwnsDocumentNavigation
|
|
1561
1689
|
&& codeBlockToolbar?.block.id === group.block.id
|
|
@@ -1564,6 +1692,16 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1564
1692
|
) : (
|
|
1565
1693
|
(() => {
|
|
1566
1694
|
const question = questionIndex.byBlock.get(group.block.id);
|
|
1695
|
+
const hostCard = question && renderQuestionCard
|
|
1696
|
+
? renderQuestionCard(question, answersByKey.get(question.question.key))
|
|
1697
|
+
: undefined;
|
|
1698
|
+
if (hostCard !== undefined) {
|
|
1699
|
+
return (
|
|
1700
|
+
<div key={group.block.id} data-block-id={group.block.id} data-block-type="directive">
|
|
1701
|
+
{hostCard}
|
|
1702
|
+
</div>
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1567
1705
|
return (
|
|
1568
1706
|
<BlockRenderer
|
|
1569
1707
|
imageBaseDir={imageBaseDir}
|
|
@@ -1575,21 +1713,23 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1575
1713
|
onNavigateAnchor={scrollToAnchor}
|
|
1576
1714
|
onToggleCheckbox={readOnly ? undefined : onToggleCheckbox}
|
|
1577
1715
|
checkboxOverrides={checkboxOverrides}
|
|
1578
|
-
githubRepo={
|
|
1579
|
-
repoHost={
|
|
1716
|
+
githubRepo={linkRepo?.display}
|
|
1717
|
+
repoHost={linkRepo?.host}
|
|
1580
1718
|
headingAnchorId={headingSlugMap.get(group.block.id)}
|
|
1581
1719
|
question={question}
|
|
1582
1720
|
questionTotal={questionIndex.total}
|
|
1583
1721
|
questionAnswer={question ? answersByKey.get(question.question.key) : undefined}
|
|
1584
|
-
onAnswerQuestion={readOnly ? undefined : onAnswerQuestion}
|
|
1585
|
-
onSaveQuestionAnswer={readOnly ? undefined : onSaveQuestionAnswer}
|
|
1722
|
+
onAnswerQuestion={readOnly || (question && questionReadOnly?.(question)) ? undefined : onAnswerQuestion}
|
|
1723
|
+
onSaveQuestionAnswer={readOnly || (question && questionReadOnly?.(question)) ? undefined : onSaveQuestionAnswer}
|
|
1586
1724
|
saveQuestionAnswerLabel={saveQuestionAnswerLabel}
|
|
1587
1725
|
renderQuestionFooter={renderQuestionFooter}
|
|
1588
1726
|
questionDecisionRecording={question ? questionDecisionRecording?.(question) : undefined}
|
|
1589
1727
|
questionDecisionScope={questionDecisionScope}
|
|
1590
|
-
onToggleQuestionDecisionRecording={readOnly ? undefined : onToggleQuestionDecisionRecording}
|
|
1591
|
-
onOpenQuestionDecision={readOnly ? undefined : onOpenQuestionDecision}
|
|
1728
|
+
onToggleQuestionDecisionRecording={readOnly || (question && (questionDecisionLocked?.(question) || questionReadOnly?.(question))) ? undefined : onToggleQuestionDecisionRecording}
|
|
1729
|
+
onOpenQuestionDecision={readOnly || (question && (questionDecisionLocked?.(question) || questionReadOnly?.(question))) ? undefined : onOpenQuestionDecision}
|
|
1592
1730
|
questionStatusTag={questionStatusTag}
|
|
1731
|
+
questionStatusLabel={questionStatusLabel}
|
|
1732
|
+
renderQuestionBelowPrompt={renderQuestionBelowPrompt}
|
|
1593
1733
|
/>
|
|
1594
1734
|
);
|
|
1595
1735
|
})()
|
|
@@ -1597,7 +1737,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1597
1737
|
)}
|
|
1598
1738
|
|
|
1599
1739
|
{/* Text selection toolbar */}
|
|
1600
|
-
{!
|
|
1740
|
+
{!authoringOff && !hostComposes && toolbarState && (
|
|
1601
1741
|
<ToolbarErrorBoundary>
|
|
1602
1742
|
<AnnotationToolbar
|
|
1603
1743
|
element={toolbarState.element}
|
|
@@ -1651,7 +1791,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1651
1791
|
)}
|
|
1652
1792
|
|
|
1653
1793
|
{/* Code block hover toolbar */}
|
|
1654
|
-
{!
|
|
1794
|
+
{!authoringOff
|
|
1655
1795
|
&& codeBlockToolbar
|
|
1656
1796
|
&& !toolbarState
|
|
1657
1797
|
&& !(vimOwnsDocumentNavigation && codeBlockToolbar.activation === 'pointer')
|
|
@@ -1701,8 +1841,8 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1701
1841
|
onImageClick={(src, alt) => setLightbox({ src, alt })}
|
|
1702
1842
|
onOpenLinkedDoc={onOpenLinkedDoc}
|
|
1703
1843
|
onOpenCodeFile={onOpenCodeFile}
|
|
1704
|
-
githubRepo={
|
|
1705
|
-
repoHost={
|
|
1844
|
+
githubRepo={linkRepo?.display}
|
|
1845
|
+
repoHost={linkRepo?.host}
|
|
1706
1846
|
onNavigateAnchor={scrollToAnchor}
|
|
1707
1847
|
/>
|
|
1708
1848
|
)}
|
|
@@ -1736,7 +1876,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1736
1876
|
)}
|
|
1737
1877
|
|
|
1738
1878
|
{/* Comment popover — hook handles text selection, Viewer handles global + code block */}
|
|
1739
|
-
{!
|
|
1879
|
+
{!authoringOff && !hostComposes && hookCommentPopover && (
|
|
1740
1880
|
<CommentPopover
|
|
1741
1881
|
anchorEl={hookCommentPopover.anchorEl}
|
|
1742
1882
|
contextText={hookCommentPopover.contextText}
|
|
@@ -1758,7 +1898,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1758
1898
|
}}
|
|
1759
1899
|
/>
|
|
1760
1900
|
)}
|
|
1761
|
-
{!
|
|
1901
|
+
{!authoringOff && viewerCommentPopover && (
|
|
1762
1902
|
<CommentPopover
|
|
1763
1903
|
anchorEl={viewerCommentPopover.anchorEl}
|
|
1764
1904
|
contextText={viewerCommentPopover.contextText}
|
|
@@ -1788,7 +1928,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1788
1928
|
)}
|
|
1789
1929
|
|
|
1790
1930
|
{/* Quick Label floating picker — hook handles text selection, Viewer handles code blocks */}
|
|
1791
|
-
{!
|
|
1931
|
+
{!authoringOff && !hostComposes && hookQuickLabelPicker && (
|
|
1792
1932
|
<FloatingQuickLabelPicker
|
|
1793
1933
|
anchorEl={hookQuickLabelPicker.anchorEl}
|
|
1794
1934
|
cursorHint={hookQuickLabelPicker.cursorHint}
|
|
@@ -1796,7 +1936,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1796
1936
|
onDismiss={hookQuickLabelPickerDismiss}
|
|
1797
1937
|
/>
|
|
1798
1938
|
)}
|
|
1799
|
-
{!
|
|
1939
|
+
{!authoringOff && codeBlockQuickLabelPicker && (
|
|
1800
1940
|
<FloatingQuickLabelPicker
|
|
1801
1941
|
anchorEl={codeBlockQuickLabelPicker.anchorEl}
|
|
1802
1942
|
onSelect={(label: QuickLabel) => {
|
|
@@ -1820,11 +1960,12 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1820
1960
|
|
|
1821
1961
|
{/* Image lightbox */}
|
|
1822
1962
|
{lightbox && createPortal(
|
|
1823
|
-
<ImageLightbox src={lightbox.src} alt={lightbox.alt} onClose={() =>
|
|
1963
|
+
<ImageLightbox src={lightbox.src} alt={lightbox.alt} images={lightbox.images} onClose={() => setLightboxState(null)} />,
|
|
1824
1964
|
document.body
|
|
1825
1965
|
)}
|
|
1826
1966
|
</div>
|
|
1827
1967
|
</DiagramAnchorClaimsContext.Provider>
|
|
1968
|
+
</LocalLinkContext.Provider>
|
|
1828
1969
|
</CodePathValidationContext.Provider>
|
|
1829
1970
|
);
|
|
1830
1971
|
});
|
|
@@ -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
|
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The muted line under an Ask AI provider bar that says how to get "Ask this
|
|
5
|
+
* session" (text from askSessionSetupHint). Renders nothing without text.
|
|
6
|
+
*/
|
|
7
|
+
export const AskSessionSetupHint: React.FC<{ text?: string | null }> = ({ text }) =>
|
|
8
|
+
text ? (
|
|
9
|
+
<div className="-mt-1 px-2 pb-1.5 text-[10px] text-muted-foreground/70" data-ai-ask-session-setup-hint>
|
|
10
|
+
{text}
|
|
11
|
+
</div>
|
|
12
|
+
) : null;
|