@plannotator/ui 0.52.0 → 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 +317 -29
- package/README.md +35 -3
- package/components/AnnotationPanel.tsx +159 -107
- package/components/AnnotationToolbar.tsx +17 -14
- package/components/AnnotationToolstrip.tsx +9 -0
- package/components/BlockRenderer.tsx +26 -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 +214 -55
- 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 +253 -104
- 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. */
|
|
@@ -218,21 +247,65 @@ export interface ViewerProps {
|
|
|
218
247
|
* decision"), given the indexed question and its saved answer. Rendered in
|
|
219
248
|
* read-only cards too; return null for none. */
|
|
220
249
|
renderQuestionFooter?: (question: IndexedQuestion, answer: QuestionAnswer | undefined) => React.ReactNode;
|
|
221
|
-
/** Whether answering a
|
|
222
|
-
*
|
|
223
|
-
* tag dimmed with a dotted outline and
|
|
224
|
-
* decision" row; true or undefined keeps today's card.
|
|
225
|
-
* question
|
|
250
|
+
/** Whether answering a question records a decision, per question. On a
|
|
251
|
+
* `Decision: when answered` question: false draws its "Records a
|
|
252
|
+
* decision" tag dimmed with a dotted outline and hides the "Answering this
|
|
253
|
+
* records a decision" row; true or undefined keeps today's card. On a
|
|
254
|
+
* question with no decision line it is ignored unless
|
|
255
|
+
* `questionDecisionScope` is `'any'`; then a boolean adds the tag in that
|
|
256
|
+
* state (true also shows the row) and undefined adds nothing. Has no
|
|
257
|
+
* effect on a question whose decision is already recorded. */
|
|
226
258
|
questionDecisionRecording?: (question: IndexedQuestion) => boolean | undefined;
|
|
227
|
-
/**
|
|
228
|
-
*
|
|
229
|
-
*
|
|
259
|
+
/** Which questions can carry the "Records a decision" tag (ui 0.52.1).
|
|
260
|
+
* `'when-answered'` (default, the 0.52.0 behaviour): only questions with
|
|
261
|
+
* `Decision: when answered`. `'any'`: also a question with no decision
|
|
262
|
+
* line, when `questionDecisionRecording` returns a boolean for it. */
|
|
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;
|
|
269
|
+
/** Makes a question's "Records a decision" tag a toggle button
|
|
270
|
+
* (`aria-pressed`); a click calls this with the question's key and the
|
|
271
|
+
* next state, which the host stores and returns from
|
|
230
272
|
* `questionDecisionRecording`. Ignored when `readOnly`. */
|
|
231
273
|
onToggleQuestionDecisionRecording?: (key: string, next: boolean) => void;
|
|
274
|
+
/** Opens the host's decision card (ui 0.52.1): the tag splits into a
|
|
275
|
+
* diamond switch (the toggle above) and the words "Records a decision" as
|
|
276
|
+
* a button that calls this with the question's key and the words' element
|
|
277
|
+
* (anchor your popover to it). Offered on and off. Ignored when
|
|
278
|
+
* `readOnly`. */
|
|
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;
|
|
232
291
|
/** `'none'` hides each question card's own status tag (Open / Answered /
|
|
233
292
|
* Settled / Skipped) so the host can draw its own; the decision tags stay.
|
|
234
293
|
* Default `'card'`. */
|
|
235
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;
|
|
236
309
|
onAskAI?: CommentAskAIHandler;
|
|
237
310
|
/** Whether comment popovers offer image attachments. Hosts without an
|
|
238
311
|
* uploadTransport pass false so the attach affordance never dead-ends.
|
|
@@ -243,6 +316,16 @@ export interface ViewerProps {
|
|
|
243
316
|
* comment, attachments, checkbox toggles). Existing annotations still
|
|
244
317
|
* render and remain selectable. Default false — today's behavior. */
|
|
245
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;
|
|
246
329
|
/** Opt-in Vim-style keyboard selection. Default false for compatibility. */
|
|
247
330
|
vimModeEnabled?: boolean;
|
|
248
331
|
/** Replace the compact Vim badge with the live video-style key HUD. */
|
|
@@ -254,12 +337,36 @@ export interface ViewerProps {
|
|
|
254
337
|
/** Fires once per highlight-restore pass with what it tried and what it could
|
|
255
338
|
* not anchor, so a host can mark the leftovers in its annotation panel. */
|
|
256
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;
|
|
257
352
|
}
|
|
258
353
|
|
|
259
354
|
export interface ViewerHandle {
|
|
260
355
|
removeHighlight: (id: string) => void;
|
|
261
356
|
clearAllHighlights: () => void;
|
|
262
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;
|
|
263
370
|
}
|
|
264
371
|
|
|
265
372
|
interface CodeBlockToolbarTarget {
|
|
@@ -574,6 +681,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
574
681
|
onAddGlobalAttachment,
|
|
575
682
|
onRemoveGlobalAttachment,
|
|
576
683
|
repoInfo,
|
|
684
|
+
refRepo,
|
|
577
685
|
stickyActions = true,
|
|
578
686
|
annotationHeader,
|
|
579
687
|
gridEnabled = false,
|
|
@@ -587,8 +695,11 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
587
695
|
maxWidth,
|
|
588
696
|
onOpenLinkedDoc,
|
|
589
697
|
onOpenCodeFile,
|
|
698
|
+
resolveLocalLink,
|
|
590
699
|
linkedDocInfo,
|
|
591
700
|
imageBaseDir,
|
|
701
|
+
onImageClick: hostImageClick,
|
|
702
|
+
imageLightboxStepping = false,
|
|
592
703
|
codePathBaseDir,
|
|
593
704
|
disableCodePathValidation,
|
|
594
705
|
onRestoreReport,
|
|
@@ -606,20 +717,45 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
606
717
|
saveQuestionAnswerLabel,
|
|
607
718
|
renderQuestionFooter,
|
|
608
719
|
questionDecisionRecording,
|
|
720
|
+
questionDecisionScope,
|
|
721
|
+
questionDecisionLines = true,
|
|
609
722
|
onToggleQuestionDecisionRecording,
|
|
723
|
+
onOpenQuestionDecision,
|
|
724
|
+
questionDecisionLocked,
|
|
725
|
+
questionReadOnly,
|
|
610
726
|
questionStatusTag,
|
|
727
|
+
questionStatusLabel,
|
|
728
|
+
renderQuestionCard,
|
|
729
|
+
renderQuestionBelowPrompt,
|
|
611
730
|
onAskAI,
|
|
612
731
|
allowImages = true,
|
|
613
732
|
readOnly = false,
|
|
733
|
+
answerOnly = false,
|
|
614
734
|
vimModeEnabled = false,
|
|
615
735
|
vimHudEnabled = false,
|
|
616
736
|
vimHudKeyPanelEnabled = true,
|
|
617
737
|
onVimHudKeyPanelChange,
|
|
738
|
+
onHostDraft,
|
|
618
739
|
}, ref) => {
|
|
619
|
-
|
|
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;
|
|
620
745
|
const hasViewerAnnotationHeader = viewerAnnotationHeader !== undefined;
|
|
746
|
+
// Where `#123` / `@user` link: `refRepo` when the host gave one, else `repoInfo`.
|
|
747
|
+
const linkRepo = refRepo ?? repoInfo;
|
|
621
748
|
const [copied, setCopied] = useState(false);
|
|
622
|
-
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
|
+
};
|
|
623
759
|
const [locationHash, setLocationHash] = useState(() => window.location.hash);
|
|
624
760
|
const globalCommentButtonRef = useRef<HTMLButtonElement>(null);
|
|
625
761
|
// Read through a ref: only the imperative removeHighlight path below needs
|
|
@@ -674,9 +810,9 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
674
810
|
// annotations that carry `questionAnswer` (matched by question key, so an
|
|
675
811
|
// answer follows its question across re-parses).
|
|
676
812
|
const questionIndex = useMemo(() => {
|
|
677
|
-
const list = indexQuestionBlocks(blocks);
|
|
813
|
+
const list = indexQuestionBlocks(blocks, { decisionLines: questionDecisionLines });
|
|
678
814
|
return { byBlock: new Map(list.map((q) => [q.blockId, q])), total: list.length };
|
|
679
|
-
}, [blocks]);
|
|
815
|
+
}, [blocks, questionDecisionLines]);
|
|
680
816
|
// A host that keeps answers itself (`questionAnswers`) replaces the
|
|
681
817
|
// annotation-derived map; each entry is validated like any other reader.
|
|
682
818
|
const answersByKey = useMemo(
|
|
@@ -706,8 +842,8 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
706
842
|
codeBlock: { block: Block; element: HTMLElement };
|
|
707
843
|
} | null>(null);
|
|
708
844
|
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
709
|
-
const readOnlyRef = useRef(
|
|
710
|
-
readOnlyRef.current =
|
|
845
|
+
const readOnlyRef = useRef(authoringOff);
|
|
846
|
+
readOnlyRef.current = authoringOff;
|
|
711
847
|
const stickySentinelRef = useRef<HTMLDivElement>(null);
|
|
712
848
|
const lastAutoScrolledHashRef = useRef<string | null>(null);
|
|
713
849
|
const [isStuck, setIsStuck] = useState(false);
|
|
@@ -739,6 +875,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
739
875
|
removeHighlight: hookRemoveHighlight,
|
|
740
876
|
clearAllHighlights,
|
|
741
877
|
applyAnnotations,
|
|
878
|
+
takeSelection,
|
|
742
879
|
} = useAnnotationHighlighter({
|
|
743
880
|
containerRef,
|
|
744
881
|
annotations,
|
|
@@ -746,7 +883,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
746
883
|
onSelectAnnotation,
|
|
747
884
|
selectedAnnotationId,
|
|
748
885
|
mode,
|
|
749
|
-
enabled: !
|
|
886
|
+
enabled: !authoringOff,
|
|
750
887
|
// Markdown documents drift between the moment a draft/share is written and
|
|
751
888
|
// the moment it is restored (a plan revision, a re-rendered block, a
|
|
752
889
|
// renderer change that adds or drops elements — #1509 hoisted an alert's
|
|
@@ -758,6 +895,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
758
895
|
verifyRestoredContent: true,
|
|
759
896
|
onRestoreMismatch: handleRestoreMismatch,
|
|
760
897
|
onRestoreReport,
|
|
898
|
+
onHostDraft: hostComposes ? onHostDraft : undefined,
|
|
761
899
|
});
|
|
762
900
|
|
|
763
901
|
// Refs for code block annotation path
|
|
@@ -929,7 +1067,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
929
1067
|
});
|
|
930
1068
|
}, [applyCodeBlockAnnotation, blocks]);
|
|
931
1069
|
|
|
932
|
-
const vimModeActive = vimModeEnabled && !
|
|
1070
|
+
const vimModeActive = vimModeEnabled && !authoringOff;
|
|
933
1071
|
const keyboardCodeBlockToolbarOpen = codeBlockToolbar?.activation === 'keyboard';
|
|
934
1072
|
const vimBlocked = !!toolbarState
|
|
935
1073
|
|| !!hookCommentPopover
|
|
@@ -958,12 +1096,16 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
958
1096
|
onHandledCommand: handleVimCommand,
|
|
959
1097
|
});
|
|
960
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(() => {}, []);
|
|
961
1102
|
const { hoverTarget, clearHover: clearPinpointHover } = usePinpoint({
|
|
962
1103
|
containerRef,
|
|
963
1104
|
inputMethod,
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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,
|
|
967
1109
|
});
|
|
968
1110
|
clearPinpointHoverRef.current = clearPinpointHover;
|
|
969
1111
|
const vimOwnsHudTarget = vimHudEnabled
|
|
@@ -982,7 +1124,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
982
1124
|
: (inputMethod === 'pinpoint' ? hoverTarget : null) ?? legacyVimTarget;
|
|
983
1125
|
|
|
984
1126
|
useEffect(() => {
|
|
985
|
-
if (!
|
|
1127
|
+
if (!authoringOff) return;
|
|
986
1128
|
if (hoverTimeoutRef.current) {
|
|
987
1129
|
clearTimeout(hoverTimeoutRef.current);
|
|
988
1130
|
hoverTimeoutRef.current = null;
|
|
@@ -991,7 +1133,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
991
1133
|
setIsCodeBlockToolbarExiting(false);
|
|
992
1134
|
setViewerCommentPopover(null);
|
|
993
1135
|
setCodeBlockQuickLabelPicker(null);
|
|
994
|
-
}, [
|
|
1136
|
+
}, [authoringOff]);
|
|
995
1137
|
|
|
996
1138
|
useEffect(() => {
|
|
997
1139
|
if (!vimOwnsDocumentNavigation) return;
|
|
@@ -1137,7 +1279,8 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1137
1279
|
},
|
|
1138
1280
|
clearAllHighlights,
|
|
1139
1281
|
applySharedAnnotations: applyAnnotations,
|
|
1140
|
-
|
|
1282
|
+
takeSelection,
|
|
1283
|
+
}), [hookRemoveHighlight, clearAllHighlights, applyAnnotations, blocks, takeSelection]);
|
|
1141
1284
|
|
|
1142
1285
|
// --- Viewer-specific: code block annotation ---
|
|
1143
1286
|
|
|
@@ -1250,7 +1393,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1250
1393
|
</button>
|
|
1251
1394
|
)}
|
|
1252
1395
|
|
|
1253
|
-
{!
|
|
1396
|
+
{!authoringOff && onAddGlobalAttachment && onRemoveGlobalAttachment && (
|
|
1254
1397
|
<AttachmentsButton
|
|
1255
1398
|
images={globalAttachments}
|
|
1256
1399
|
onAdd={onAddGlobalAttachment}
|
|
@@ -1260,7 +1403,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1260
1403
|
/>
|
|
1261
1404
|
)}
|
|
1262
1405
|
|
|
1263
|
-
{!
|
|
1406
|
+
{!authoringOff && (
|
|
1264
1407
|
<button
|
|
1265
1408
|
ref={globalCommentButtonRef}
|
|
1266
1409
|
onClick={() => {
|
|
@@ -1332,6 +1475,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1332
1475
|
|
|
1333
1476
|
return (
|
|
1334
1477
|
<CodePathValidationContext.Provider value={codePathValidation}>
|
|
1478
|
+
<LocalLinkContext.Provider value={resolveLocalLink ?? null}>
|
|
1335
1479
|
<DiagramAnchorClaimsContext.Provider value={diagramClaims}>
|
|
1336
1480
|
<div className="relative z-50 w-full" style={maxWidth === null ? undefined : { maxWidth: maxWidth ?? 832 }}>
|
|
1337
1481
|
{taterMode && <TaterSpriteSitting />}
|
|
@@ -1403,7 +1547,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1403
1547
|
<>
|
|
1404
1548
|
{badgeClearance > 0 && <div data-print-hide style={{ height: badgeClearance }} aria-hidden="true" />}
|
|
1405
1549
|
{stickyActions && <div ref={stickySentinelRef} className="h-0 w-0 float-right" aria-hidden="true" />}
|
|
1406
|
-
{stickyActions ? (
|
|
1550
|
+
{answerOnly || hostComposes ? null : stickyActions ? (
|
|
1407
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`}>
|
|
1408
1552
|
{documentActions}
|
|
1409
1553
|
</StickyActionsLane>
|
|
@@ -1433,8 +1577,8 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1433
1577
|
onOpenCodeFile={onOpenCodeFile}
|
|
1434
1578
|
onToggleCheckbox={readOnly ? undefined : onToggleCheckbox}
|
|
1435
1579
|
checkboxOverrides={checkboxOverrides}
|
|
1436
|
-
githubRepo={
|
|
1437
|
-
repoHost={
|
|
1580
|
+
githubRepo={linkRepo?.display}
|
|
1581
|
+
repoHost={linkRepo?.host}
|
|
1438
1582
|
headingAnchorId={headingSlugMap.get(block.id)}
|
|
1439
1583
|
onNavigateAnchor={scrollToAnchor}
|
|
1440
1584
|
/>
|
|
@@ -1449,10 +1593,10 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1449
1593
|
annotations={annotations}
|
|
1450
1594
|
selectedAnnotationId={selectedAnnotationId}
|
|
1451
1595
|
onSelectAnnotation={onSelectAnnotation}
|
|
1452
|
-
onAddAnnotation={
|
|
1453
|
-
readOnly={
|
|
1596
|
+
onAddAnnotation={authoringOff ? undefined : onAddAnnotation}
|
|
1597
|
+
readOnly={authoringOff}
|
|
1454
1598
|
onRestoreReport={onRestoreReport}
|
|
1455
|
-
onAskAI={
|
|
1599
|
+
onAskAI={authoringOff ? undefined : onAskAI}
|
|
1456
1600
|
askAISourcePath={linkedDocInfo?.filepath ?? sourceInfo}
|
|
1457
1601
|
/>
|
|
1458
1602
|
</Suspense>
|
|
@@ -1463,10 +1607,10 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1463
1607
|
annotations={annotations}
|
|
1464
1608
|
selectedAnnotationId={selectedAnnotationId}
|
|
1465
1609
|
onSelectAnnotation={onSelectAnnotation}
|
|
1466
|
-
onAddAnnotation={
|
|
1467
|
-
readOnly={
|
|
1610
|
+
onAddAnnotation={authoringOff ? undefined : onAddAnnotation}
|
|
1611
|
+
readOnly={authoringOff}
|
|
1468
1612
|
onRestoreReport={onRestoreReport}
|
|
1469
|
-
onAskAI={
|
|
1613
|
+
onAskAI={authoringOff ? undefined : onAskAI}
|
|
1470
1614
|
askAISourcePath={linkedDocInfo?.filepath ?? sourceInfo}
|
|
1471
1615
|
/>
|
|
1472
1616
|
</Suspense>
|
|
@@ -1478,8 +1622,8 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1478
1622
|
onImageClick={(src, alt) => setLightbox({ src, alt })}
|
|
1479
1623
|
onOpenLinkedDoc={onOpenLinkedDoc}
|
|
1480
1624
|
onOpenCodeFile={onOpenCodeFile}
|
|
1481
|
-
githubRepo={
|
|
1482
|
-
repoHost={
|
|
1625
|
+
githubRepo={linkRepo?.display}
|
|
1626
|
+
repoHost={linkRepo?.host}
|
|
1483
1627
|
onNavigateAnchor={scrollToAnchor}
|
|
1484
1628
|
onHover={(element) => {
|
|
1485
1629
|
if (tableHoverTimeoutRef.current) {
|
|
@@ -1487,7 +1631,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1487
1631
|
tableHoverTimeoutRef.current = null;
|
|
1488
1632
|
}
|
|
1489
1633
|
setIsTableToolbarExiting(false);
|
|
1490
|
-
if (!toolbarState) {
|
|
1634
|
+
if (!toolbarState && !hostComposes) {
|
|
1491
1635
|
setHoveredTable({ block: group.block, element });
|
|
1492
1636
|
}
|
|
1493
1637
|
}}
|
|
@@ -1505,7 +1649,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1505
1649
|
<CodeBlock
|
|
1506
1650
|
key={group.block.id}
|
|
1507
1651
|
block={group.block}
|
|
1508
|
-
onHover={
|
|
1652
|
+
onHover={authoringOff || hostComposes || inputMethod === 'pinpoint' ? undefined : (element) => {
|
|
1509
1653
|
// Clear any pending leave timeout
|
|
1510
1654
|
if (hoverTimeoutRef.current) {
|
|
1511
1655
|
clearTimeout(hoverTimeoutRef.current);
|
|
@@ -1526,7 +1670,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1526
1670
|
});
|
|
1527
1671
|
}
|
|
1528
1672
|
}}
|
|
1529
|
-
onLeave={
|
|
1673
|
+
onLeave={authoringOff || hostComposes || inputMethod === 'pinpoint' ? undefined : () => {
|
|
1530
1674
|
if (keyboardCodeBlockToolbarOpen) return;
|
|
1531
1675
|
// Delay then start exit animation
|
|
1532
1676
|
hoverTimeoutRef.current = setTimeout(() => {
|
|
@@ -1539,7 +1683,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1539
1683
|
}, 100);
|
|
1540
1684
|
}}
|
|
1541
1685
|
isHovered={
|
|
1542
|
-
!
|
|
1686
|
+
!authoringOff
|
|
1543
1687
|
&& inputMethod !== 'pinpoint'
|
|
1544
1688
|
&& !vimOwnsDocumentNavigation
|
|
1545
1689
|
&& codeBlockToolbar?.block.id === group.block.id
|
|
@@ -1548,6 +1692,16 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1548
1692
|
) : (
|
|
1549
1693
|
(() => {
|
|
1550
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
|
+
}
|
|
1551
1705
|
return (
|
|
1552
1706
|
<BlockRenderer
|
|
1553
1707
|
imageBaseDir={imageBaseDir}
|
|
@@ -1559,19 +1713,23 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1559
1713
|
onNavigateAnchor={scrollToAnchor}
|
|
1560
1714
|
onToggleCheckbox={readOnly ? undefined : onToggleCheckbox}
|
|
1561
1715
|
checkboxOverrides={checkboxOverrides}
|
|
1562
|
-
githubRepo={
|
|
1563
|
-
repoHost={
|
|
1716
|
+
githubRepo={linkRepo?.display}
|
|
1717
|
+
repoHost={linkRepo?.host}
|
|
1564
1718
|
headingAnchorId={headingSlugMap.get(group.block.id)}
|
|
1565
1719
|
question={question}
|
|
1566
1720
|
questionTotal={questionIndex.total}
|
|
1567
1721
|
questionAnswer={question ? answersByKey.get(question.question.key) : undefined}
|
|
1568
|
-
onAnswerQuestion={readOnly ? undefined : onAnswerQuestion}
|
|
1569
|
-
onSaveQuestionAnswer={readOnly ? undefined : onSaveQuestionAnswer}
|
|
1722
|
+
onAnswerQuestion={readOnly || (question && questionReadOnly?.(question)) ? undefined : onAnswerQuestion}
|
|
1723
|
+
onSaveQuestionAnswer={readOnly || (question && questionReadOnly?.(question)) ? undefined : onSaveQuestionAnswer}
|
|
1570
1724
|
saveQuestionAnswerLabel={saveQuestionAnswerLabel}
|
|
1571
1725
|
renderQuestionFooter={renderQuestionFooter}
|
|
1572
1726
|
questionDecisionRecording={question ? questionDecisionRecording?.(question) : undefined}
|
|
1573
|
-
|
|
1727
|
+
questionDecisionScope={questionDecisionScope}
|
|
1728
|
+
onToggleQuestionDecisionRecording={readOnly || (question && (questionDecisionLocked?.(question) || questionReadOnly?.(question))) ? undefined : onToggleQuestionDecisionRecording}
|
|
1729
|
+
onOpenQuestionDecision={readOnly || (question && (questionDecisionLocked?.(question) || questionReadOnly?.(question))) ? undefined : onOpenQuestionDecision}
|
|
1574
1730
|
questionStatusTag={questionStatusTag}
|
|
1731
|
+
questionStatusLabel={questionStatusLabel}
|
|
1732
|
+
renderQuestionBelowPrompt={renderQuestionBelowPrompt}
|
|
1575
1733
|
/>
|
|
1576
1734
|
);
|
|
1577
1735
|
})()
|
|
@@ -1579,7 +1737,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1579
1737
|
)}
|
|
1580
1738
|
|
|
1581
1739
|
{/* Text selection toolbar */}
|
|
1582
|
-
{!
|
|
1740
|
+
{!authoringOff && !hostComposes && toolbarState && (
|
|
1583
1741
|
<ToolbarErrorBoundary>
|
|
1584
1742
|
<AnnotationToolbar
|
|
1585
1743
|
element={toolbarState.element}
|
|
@@ -1633,7 +1791,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1633
1791
|
)}
|
|
1634
1792
|
|
|
1635
1793
|
{/* Code block hover toolbar */}
|
|
1636
|
-
{!
|
|
1794
|
+
{!authoringOff
|
|
1637
1795
|
&& codeBlockToolbar
|
|
1638
1796
|
&& !toolbarState
|
|
1639
1797
|
&& !(vimOwnsDocumentNavigation && codeBlockToolbar.activation === 'pointer')
|
|
@@ -1683,8 +1841,8 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1683
1841
|
onImageClick={(src, alt) => setLightbox({ src, alt })}
|
|
1684
1842
|
onOpenLinkedDoc={onOpenLinkedDoc}
|
|
1685
1843
|
onOpenCodeFile={onOpenCodeFile}
|
|
1686
|
-
githubRepo={
|
|
1687
|
-
repoHost={
|
|
1844
|
+
githubRepo={linkRepo?.display}
|
|
1845
|
+
repoHost={linkRepo?.host}
|
|
1688
1846
|
onNavigateAnchor={scrollToAnchor}
|
|
1689
1847
|
/>
|
|
1690
1848
|
)}
|
|
@@ -1718,7 +1876,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1718
1876
|
)}
|
|
1719
1877
|
|
|
1720
1878
|
{/* Comment popover — hook handles text selection, Viewer handles global + code block */}
|
|
1721
|
-
{!
|
|
1879
|
+
{!authoringOff && !hostComposes && hookCommentPopover && (
|
|
1722
1880
|
<CommentPopover
|
|
1723
1881
|
anchorEl={hookCommentPopover.anchorEl}
|
|
1724
1882
|
contextText={hookCommentPopover.contextText}
|
|
@@ -1740,7 +1898,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1740
1898
|
}}
|
|
1741
1899
|
/>
|
|
1742
1900
|
)}
|
|
1743
|
-
{!
|
|
1901
|
+
{!authoringOff && viewerCommentPopover && (
|
|
1744
1902
|
<CommentPopover
|
|
1745
1903
|
anchorEl={viewerCommentPopover.anchorEl}
|
|
1746
1904
|
contextText={viewerCommentPopover.contextText}
|
|
@@ -1770,7 +1928,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1770
1928
|
)}
|
|
1771
1929
|
|
|
1772
1930
|
{/* Quick Label floating picker — hook handles text selection, Viewer handles code blocks */}
|
|
1773
|
-
{!
|
|
1931
|
+
{!authoringOff && !hostComposes && hookQuickLabelPicker && (
|
|
1774
1932
|
<FloatingQuickLabelPicker
|
|
1775
1933
|
anchorEl={hookQuickLabelPicker.anchorEl}
|
|
1776
1934
|
cursorHint={hookQuickLabelPicker.cursorHint}
|
|
@@ -1778,7 +1936,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1778
1936
|
onDismiss={hookQuickLabelPickerDismiss}
|
|
1779
1937
|
/>
|
|
1780
1938
|
)}
|
|
1781
|
-
{!
|
|
1939
|
+
{!authoringOff && codeBlockQuickLabelPicker && (
|
|
1782
1940
|
<FloatingQuickLabelPicker
|
|
1783
1941
|
anchorEl={codeBlockQuickLabelPicker.anchorEl}
|
|
1784
1942
|
onSelect={(label: QuickLabel) => {
|
|
@@ -1802,11 +1960,12 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1802
1960
|
|
|
1803
1961
|
{/* Image lightbox */}
|
|
1804
1962
|
{lightbox && createPortal(
|
|
1805
|
-
<ImageLightbox src={lightbox.src} alt={lightbox.alt} onClose={() =>
|
|
1963
|
+
<ImageLightbox src={lightbox.src} alt={lightbox.alt} images={lightbox.images} onClose={() => setLightboxState(null)} />,
|
|
1806
1964
|
document.body
|
|
1807
1965
|
)}
|
|
1808
1966
|
</div>
|
|
1809
1967
|
</DiagramAnchorClaimsContext.Provider>
|
|
1968
|
+
</LocalLinkContext.Provider>
|
|
1810
1969
|
</CodePathValidationContext.Provider>
|
|
1811
1970
|
);
|
|
1812
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;
|