@plannotator/ui 0.45.1 → 0.46.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 +52 -1
- package/README.md +1 -1
- package/components/AgentsTab.tsx +108 -108
- package/components/BlockRenderer.tsx +9 -5
- package/components/ExportModal.tsx +8 -4
- package/components/ImageAnnotator/Toolbar.tsx +13 -3
- package/components/ImageAnnotator/index.tsx +6 -2
- package/components/InlineMarkdown.tsx +14 -7
- package/components/RenderedMarkdown.tsx +3 -1
- package/components/Settings.tsx +38 -15
- package/components/Viewer.tsx +76 -18
- package/components/blocks/AlertBlock.tsx +3 -2
- package/components/blocks/Callout.tsx +3 -0
- package/components/blocks/CodeBlock.tsx +6 -2
- package/components/blocks/TableBlock.tsx +4 -0
- package/components/blocks/TablePopout.tsx +6 -2
- package/components/blocks/proseBody.tsx +3 -0
- package/components/plan-diff/PlanCleanDiffView.tsx +6 -2
- package/config/reviewView.ts +8 -3
- package/config/settings.ts +10 -4
- package/hooks/useAgentSettings.ts +108 -123
- package/hooks/useAnnotationHighlighter.ts +109 -1
- package/hooks/useCodeAnnotationDraft.ts +196 -3
- package/hooks/useModelCatalogs.ts +116 -0
- package/hooks/useVimSelection.ts +28 -7
- package/package.json +2 -2
- package/styles.css +1 -1
- package/types.ts +31 -0
- package/utils/aiProvider.ts +8 -16
- package/utils/annotateAgentInstructions.ts +162 -0
- package/utils/codeHighlight.ts +18 -0
- package/utils/parser.ts +141 -31
- package/utils/terminalToolsAnnouncement.ts +2 -2
- package/assets/review-sections.png +0 -0
- package/assets/review-tree.png +0 -0
- package/utils/codexModels.ts +0 -79
package/HANDOFF.md
CHANGED
|
@@ -1032,7 +1032,8 @@ the people and what a mention means.
|
|
|
1032
1032
|
|
|
1033
1033
|
### Threading points
|
|
1034
1034
|
|
|
1035
|
-
- `AnnotationToolbar` (`selectionActions`, `selectionActionsIcon`, `quickLabels`) — the props live here.
|
|
1035
|
+
- `AnnotationToolbar` (`selectionActions`, `selectionActionsIcon`, `quickLabels`) — the props live here.
|
|
1036
|
+
- **ui 0.45.2 (registry default):** `SETTINGS.reviewPanelView.defaultValue` is now `'tree'` (was `'sections'`). `configStore.ensureLoaded` seeds registry defaults into the host's `storageBackend`, so a host that never persisted a panel view now seeds `tree`; a persisted value still wins. No API change. Plannotator removed its first-run review setup dialog in the same change (#1587). `selectionActionsIcon?: React.ReactNode` (0.43.1) is the glyph on the wand button, forwarded by `Viewer` (both toolbars) and `HtmlViewer`; absent → the package's own wand, which 0.43.1 also simplified to one thick diagonal with a single star (the six-spark glyph read as noise at 16px). Name, `data-selection-actions`, size and behavior of the button are untouched either way.
|
|
1036
1037
|
- `Viewer` forwards both to BOTH of its toolbars (the text-selection toolbar
|
|
1037
1038
|
and the code-block hover toolbar).
|
|
1038
1039
|
- `HtmlViewer` forwards `selectionActions` to its selection toolbar. It does
|
|
@@ -1494,8 +1495,39 @@ Tests (both DOM-gated, both added to the workflow's DOM_TESTS step):
|
|
|
1494
1495
|
`components/AnnotationPanel.cardHeader.test.tsx` (6) and
|
|
1495
1496
|
`components/AnnotationPanel.editMentions.test.tsx` (10).
|
|
1496
1497
|
|
|
1498
|
+
## Discovered model catalogs (0.46.0, core 0.25.6) — BREAKING for hosts
|
|
1499
|
+
|
|
1500
|
+
Claude and Codex model lists are no longer hand-maintained in `@plannotator/ui`. They come from the installed CLIs through the Ask AI provider (`/api/ai/capabilities?activate=<id>`), in the shared `CatalogModel` shape from the new `@plannotator/core/model-catalog` subpath, with one small static fallback per provider (`CLAUDE_FALLBACK_MODELS`, `CODEX_FALLBACK_MODELS`). A host that imported any of the following must switch:
|
|
1501
|
+
|
|
1502
|
+
| Removed | Use instead |
|
|
1503
|
+
| --- | --- |
|
|
1504
|
+
| `@plannotator/ui/utils/codexModels` (the whole module: `CODEX_MODELS`, `CODEX_EFFORT_LABELS`, `codexReasoningOptions`, `clampCodexReasoning`) | `useModelCatalogs(capabilities)` for the live list, `effortSelectOptions` / `resolveEffortChoice` / `EFFORT_LABELS` from `@plannotator/core/model-catalog` |
|
|
1505
|
+
| `CLAUDE_MODELS`, `CLAUDE_EFFORT`, `TOUR_CLAUDE_MODELS`, `CODEX_MODELS`, `codexReasoningOptions` exports of `components/AgentsTab` | the same catalog helpers (`modelSelectOptions`, `effortSelectOptions`) |
|
|
1506
|
+
| `sanitizeCodexPerModel`, `migrateCodexSection` from `hooks/useAgentSettings` | nothing: saved picks now resolve at read time with `resolveModelChoice` |
|
|
1507
|
+
|
|
1508
|
+
Behaviour changes a host may notice:
|
|
1509
|
+
|
|
1510
|
+
- `DEFAULT_CODEX_MODEL` (and the tour/guide Codex defaults) are now `''`, meaning "the model the Codex list marks default"; `DEFAULT_CLAUDE_MODEL` is `'opus'` (was `'claude-opus-5'`).
|
|
1511
|
+
- `useAgentSettings(catalogs?)` takes the catalogs and returns EFFECTIVE (resolved) model/effort values; called without catalogs it returns the saved values unchanged, as before.
|
|
1512
|
+
- `AIProviderModel` is now an alias of `CatalogModel` (adds optional `resolvedId`, `fastMode`); `resolveAIModelForProvider` uses the shared resolver, so a stale pinned id keeps its model family instead of snapping to the provider default.
|
|
1513
|
+
|
|
1514
|
+
## Forge-aware `#123` / `@user` links (unreleased, additive; #1596)
|
|
1515
|
+
|
|
1516
|
+
Bare `#123` and `@user` in a document used to link to github.com whenever `githubRepo` held a slash, so GitLab and GitHub Enterprise repos got links to the wrong forge. `InlineMarkdown` (and every block component that forwards `githubRepo`: `BlockRenderer`, `RenderedMarkdown`, `TableBlock`, `TablePopout`, `AlertBlock`, `Callout`, `proseBody`) gains an optional `repoHost?: string` beside it; `Viewer` passes `repoInfo.host`. The rule is the new `@plannotator/core/forge-refs` subpath (`forgeRefLinks`, `classifyForgeHost`, `normalizeForgeHost`). The host is normalized first: lowercased, port dropped, `www.github.com` / `ssh.github.com` / SSH aliases starting `github.com-` folded to `github.com` and `altssh.gitlab.com` to `gitlab.com`; a host that is not a plausible DNS name (no dot, brackets, `@`, a dotless SSH alias) counts as absent. Then a github host (`github.com`, `github.*`) links to `https://<host>/<path>/issues/N`, a gitlab host (`gitlab.com`, `gitlab.*`, `*.gitlab.*`) to `https://<host>/<path>/-/issues/N`, users to `https://<host>/<user>`, and any other host renders the ref as an unlinked span. **A host that passes no `repoHost` (or an implausible one) keeps the old github.com links**, so nothing changes until it opts in. Because ui imports a new core subpath and `@plannotator/ui` pins `@plannotator/core` EXACTLY (currently `"0.25.6"`), the core version bump and ui's pin must move together in the same release: bump core, set ui's `@plannotator/core` dependency to that same version, `bun install`, then publish `core` first and `ui` second. A ui built against this code on the old core pin would import a `forge-refs` subpath the pinned core does not ship.
|
|
1517
|
+
|
|
1518
|
+
## Guided Review generation in core (core 0.25.6, unreleased, additive)
|
|
1519
|
+
|
|
1520
|
+
Core-only; `@plannotator/ui` imports none of it, so ui's exact `0.25.6` pin is unchanged. The pure half of Guided Review generation moved verbatim from the private `@plannotator/server` (`packages/server/guide/guide-review.ts`, which re-exports every name) so a host such as Workspaces generates guides with Plannotator's exact prompt and holds the output to the same coverage rule. All three subpaths are browser-safe and zero-dependency:
|
|
1521
|
+
|
|
1522
|
+
- `@plannotator/core/guide-prompt`: `GUIDE_REVIEW_PROMPT` (the organizer methodology), `GUIDE_SCHEMA_JSON` (the output JSON schema, as a string), `buildGuideUserMessage(patch, diffType, options?, prMetadata?, changedFiles?)`, `validateGuideOutput(raw, changedFiles)`, `GUIDE_NO_SECTIONS_ERROR`, the sanitizers `sanitizeGuideSection` / `sanitizeGuideSections` / `sanitizeUnplacedFiles`, and the types `GuideChangedFile` and `GuidePromptPRMetadata` (`{ url, baseBranch }`, structural, so a full GitHub/GitLab `PRMetadata` is accepted).
|
|
1523
|
+
- `@plannotator/core/review-prompt` (what `buildGuideUserMessage` frames a local diff with): `getLocalDiffInstruction`, `buildWorkspacePromptContextLines`, and the types `LocalDiffInstruction`, `WorkspaceReviewPromptContext`, `WorkspacePromptRepoContext`, `WorkspaceChildVcsType`.
|
|
1524
|
+
- `@plannotator/core/diff-type`: the `DiffType` union (including jj's `jj-commit:<id>`) plus `parseCommitDiffType`, `parseJjCommitDiffType`, `jjCommitRevset`, `commitFamilyId`, `parseWorktreeDiffType`, `JJ_TRUNK_REVSET`, `jjLineBaseRevset`, `jjCompareTargetRevset`, `parseRemoteBookmark`, `BARE_HEX_SHA_RE` (moved from `packages/shared/review-core.ts`, which re-exports them).
|
|
1525
|
+
|
|
1526
|
+
The guide chain's engine plumbing (Claude/Codex/marker commands, output parsing, `composeGuideMethodology`, sessions) stays in the server. It ships in core 0.25.6 alongside what that unpublished version already carries; ui needs no change for it.
|
|
1527
|
+
|
|
1497
1528
|
## Publishing & versioning
|
|
1498
1529
|
|
|
1530
|
+
- **ui 0.46.1 (fix, ui only, core pin unchanged at `0.25.6`):** `useVimSelection` (mounted by every `Viewer`) now only clears a page selection whose anchor or focus lies inside the viewer's own container; with vim off it used to clear the WHOLE page's selection on every mount and `contentVersion` change, so a selection in another host panel vanished whenever the document behind it loaded or changed.
|
|
1499
1531
|
- **ui 0.45.0 (annotation card header slot + mentions on the card's edit box): `@plannotator/ui` only — `@plannotator/core` is UNCHANGED at `0.25.5`, so this publishes alone** (core 0.25.5 must already be published). Purely additive over 0.44.0, both props on `AnnotationPanel`: `renderCardHeader` (the header-row twin of `renderCardFooter`, wrapper `[data-annotation-card-header]`, renders under `readOnly`, open-document cards only in the All-files view) and `mentionSource` (the 0.43.0 type, applied to the card's EDIT box, saving `onEdit(id, { text, mentions })` only when a source was supplied and a pick survived). Nothing is removed, no new supported imports (`components/MentionAutocomplete` is internal glue), no export-, share- or archive-visible change, and Plannotator passes neither — `packages/editor` and `packages/review-editor` have zero source diff, and the panel is byte-identical to 0.44.0. Known difference from `CommentPopover`: no chips in the card's edit box (follow-up named in the section). See "Annotation card header slot and mentions on the edit box (0.45.0)".
|
|
1500
1532
|
- **ui 0.44.0 (mention token chips in the composer): `@plannotator/ui` only — `@plannotator/core` is UNCHANGED at `0.25.5`, so this publishes alone** (core 0.25.5 must already be published). Purely additive over 0.43.2: the `@Label` tokens a `mentionSource` composer inserted render as chips in the composer's existing highlight overlay, `MentionSource.tokenClassName?` lets a host restyle them (under the metric rule), `useMentionAutocomplete` also returns the surviving `mentions`, and `utils/composerTokens` joins the supported-import list. Nothing is removed, no export-, share- or archive-visible change, and Plannotator passes none of it — with neither `mentionSource` nor `skillReferences` the composer is byte-identical to 0.43.2. See "Mention token chips in the composer (0.44.0)".
|
|
1501
1533
|
- **ui 0.43.1 (`mentionSource` on the viewers): `@plannotator/ui` only — `@plannotator/core` is UNCHANGED at `0.25.5`, so this publishes alone** (core 0.25.5 must already be published). Purely additive over 0.43.0: `mentionSource` on `Viewer` and `HtmlViewer` (forwarded to every comment composer each mounts) and the optional `Annotation.mentions` field the picked ids land on, set only when a source was supplied and a token survived. Nothing is removed, no new modules, no export-, share- or archive-visible change, and Plannotator passes none of it. See "`mentionSource` on the viewers (0.43.1)".
|
|
@@ -1517,6 +1549,25 @@ Tests (both DOM-gated, both added to the workflow's DOM_TESTS step):
|
|
|
1517
1549
|
|
|
1518
1550
|
---
|
|
1519
1551
|
|
|
1552
|
+
## `Settings` `annotateParity` (shipped in 0.46.0)
|
|
1553
|
+
|
|
1554
|
+
`<Settings mode="annotate">` takes a new optional `annotateParity?: boolean`,
|
|
1555
|
+
default `false`. **A host that omits it sees exactly the annotate Settings it
|
|
1556
|
+
had**: General, Theme, Vim, Shortcuts, Files, plus the OpenCode agent-switch
|
|
1557
|
+
row when `origin="opencode"`. Plannotator's own annotate app passes `true`,
|
|
1558
|
+
which adds the plan-review document tabs (Display, Saving, Labels, Obsidian,
|
|
1559
|
+
Bear, Octarine) and hides the agent-switch row, which only ever applied to
|
|
1560
|
+
plan approval.
|
|
1561
|
+
|
|
1562
|
+
Within those tabs the plan-decision rows stay plan-only: Save Plans and the
|
|
1563
|
+
three "Auto-save on Plan Arrival" switches. The Obsidian / Bear / Octarine
|
|
1564
|
+
enable switches are the SAME cookies plan review reads on approve, so turning
|
|
1565
|
+
one on from annotate also makes plan review save every approved plan there;
|
|
1566
|
+
the annotate description says so. `mode="plan"` and `mode="review"` are
|
|
1567
|
+
unaffected by the prop. Purely additive: no export, share or archive change.
|
|
1568
|
+
|
|
1569
|
+
---
|
|
1570
|
+
|
|
1520
1571
|
## The law (guardrails for anyone editing `@plannotator/ui`)
|
|
1521
1572
|
|
|
1522
1573
|
These are enforced socially and, where possible, by CI. They exist because a prior from-scratch reimplementation of this UI broke the app and was reverted.
|
package/README.md
CHANGED
|
@@ -367,7 +367,7 @@ npm install @plannotator/ui @plannotator/core
|
|
|
367
367
|
- `@plannotator/core` — pure utils + types, zero deps, browser-safe (CI enforces no `node:` imports). Published.
|
|
368
368
|
- `@plannotator/ui` — React components/hooks + theme + `configure()`. Depends on an exact published `@plannotator/core` version. Published.
|
|
369
369
|
- `@plannotator/shared`, `@plannotator/ai` — stay private to the monorepo; `shared` re-exports `core`'s modules via shims so Plannotator's internals are untouched.
|
|
370
|
-
- Currently `@plannotator/ui` 0.
|
|
370
|
+
- Currently `@plannotator/ui` 0.46.1 depends exactly on `@plannotator/core` 0.25.6. `core` is bumped only when something under `packages/core` changes, so `ui` can advance alone. Keep the published core version exact in `packages/ui/package.json`; do not use a `workspace:` protocol there, because a directly published manifest must remain installable outside this monorepo. Bun still links the matching local workspace during development. When both packages change, publish `core` first, then build and publish the UI tarball. See HANDOFF.md "Publishing & versioning" for the verification command.
|
|
371
371
|
|
|
372
372
|
## The one rule
|
|
373
373
|
|
package/components/AgentsTab.tsx
CHANGED
|
@@ -23,51 +23,27 @@ import { useAgentSettings } from '../hooks/useAgentSettings';
|
|
|
23
23
|
import type { AgentEngine, AgentMode, ReviewEngine } from '../hooks/useAgentSettings';
|
|
24
24
|
import type { AgentLaunchParams } from '../hooks/useAgentJobs';
|
|
25
25
|
import { ConfigRow, SegmentedPicker, Toggle, SelectMenu } from './AgentControls';
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
CLAUDE_FALLBACK_MODELS,
|
|
28
|
+
CODEX_FALLBACK_MODELS,
|
|
29
|
+
EFFORT_LABELS,
|
|
30
|
+
effortSelectOptions,
|
|
31
|
+
modelLabel,
|
|
32
|
+
modelSelectOptions,
|
|
33
|
+
type CatalogModel,
|
|
34
|
+
} from '@plannotator/core/model-catalog';
|
|
35
|
+
import { useModelCatalogs, type ModelCatalogs } from '../hooks/useModelCatalogs';
|
|
27
36
|
|
|
28
37
|
export type { AgentLaunchParams } from '../hooks/useAgentJobs';
|
|
29
38
|
|
|
30
|
-
// --- Agent option catalogs
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
{ value: 'claude-fable-5', label: 'Fable 5' },
|
|
34
|
-
{ value: 'claude-opus-5', label: 'Opus 5' },
|
|
35
|
-
{ value: 'claude-opus-4-8', label: 'Opus 4.8' },
|
|
36
|
-
{ value: 'claude-opus-4-8[1m]', label: 'Opus 4.8 (1M)' },
|
|
37
|
-
{ value: 'claude-sonnet-5', label: 'Sonnet 5' },
|
|
38
|
-
{ value: 'claude-sonnet-4-6', label: 'Sonnet 4.6' },
|
|
39
|
-
{ value: 'claude-sonnet-4-6[1m]', label: 'Sonnet 4.6 (1M)' },
|
|
40
|
-
{ value: 'claude-opus-4-7', label: 'Opus 4.7' },
|
|
41
|
-
{ value: 'claude-opus-4-7[1m]', label: 'Opus 4.7 (1M)' },
|
|
42
|
-
{ value: 'claude-opus-4-6', label: 'Opus 4.6' },
|
|
43
|
-
{ value: 'claude-opus-4-6[1m]', label: 'Opus 4.6 (1M)' },
|
|
44
|
-
{ value: 'claude-haiku-4-5', label: 'Haiku 4.5' },
|
|
45
|
-
];
|
|
39
|
+
// --- Agent option catalogs ---
|
|
40
|
+
// Claude and Codex models come from the installed CLIs (useModelCatalogs —
|
|
41
|
+
// the same lists Ask AI shows); only the marker-engine fallbacks live here.
|
|
46
42
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
{ value: 'xhigh', label: 'XHigh' },
|
|
52
|
-
{ value: 'max', label: 'Max' },
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
// Codex model catalog + per-model reasoning efforts live in
|
|
56
|
-
// utils/codexModels (useAgentSettings needs them too, for effort clamping);
|
|
57
|
-
// re-exported here so both launch surfaces keep one import site.
|
|
58
|
-
export { CODEX_MODELS, codexReasoningOptions } from '../utils/codexModels';
|
|
59
|
-
|
|
60
|
-
// Tour/guide Claude catalog: the CLI's latest-resolving aliases on top
|
|
61
|
-
// (verified against `claude --help`: "Provide an alias for the latest model
|
|
62
|
-
// (e.g. 'fable', 'opus', or 'sonnet') or a model's full name"), then every
|
|
63
|
-
// pinned version from the review catalog.
|
|
64
|
-
// Also reused by GuideEmptyState (packages/review-editor).
|
|
65
|
-
export const TOUR_CLAUDE_MODELS: Array<{ value: string; label: string }> = [
|
|
66
|
-
{ value: 'sonnet', label: 'Sonnet (latest)' },
|
|
67
|
-
{ value: 'opus', label: 'Opus (latest)' },
|
|
68
|
-
{ value: 'fable', label: 'Fable (latest)' },
|
|
69
|
-
...CLAUDE_MODELS,
|
|
70
|
-
];
|
|
43
|
+
/** Whether a model offers a fast tier (Codex's fast mode toggle). */
|
|
44
|
+
export function modelSupportsFast(models: readonly CatalogModel[], modelId: string): boolean {
|
|
45
|
+
return models.find((m) => m.id === modelId)?.fastMode === true;
|
|
46
|
+
}
|
|
71
47
|
|
|
72
48
|
// Fallback Cursor model catalog (just `auto`). The real, account-specific list
|
|
73
49
|
// is discovered server-side via `agent models` and delivered on the cursor
|
|
@@ -227,18 +203,24 @@ function catalogLabel(list: Array<{ value: string; label: string }>, value: stri
|
|
|
227
203
|
return list.find((o) => o.value === value)?.label ?? value;
|
|
228
204
|
}
|
|
229
205
|
|
|
230
|
-
|
|
206
|
+
type ModelLabelCatalogs = Pick<ModelCatalogs, 'claude' | 'codex'>;
|
|
207
|
+
|
|
208
|
+
function formatModel(provider: string, engine: string | undefined, model: string, catalogs: ModelLabelCatalogs): string {
|
|
209
|
+
// Claude/Codex: the discovered catalog's label (fallback list, then alias
|
|
210
|
+
// naming, then the raw id for anything neither knows).
|
|
211
|
+
const claudeLabel = () => modelLabel([...catalogs.claude.models, ...CLAUDE_FALLBACK_MODELS], model);
|
|
212
|
+
const codexLabel = () => modelLabel([...catalogs.codex.models, ...CODEX_FALLBACK_MODELS], model);
|
|
231
213
|
if (provider === 'cursor') return catalogLabel(CURSOR_MODELS, model);
|
|
232
214
|
if (provider === 'opencode') return model ? model : 'Default';
|
|
233
215
|
if (provider === 'pi') return model || 'Default';
|
|
234
216
|
if (provider === 'copilot') return model || 'Default';
|
|
235
|
-
if (provider === 'codex' || engine === 'codex') return
|
|
236
|
-
if ((provider === 'tour' || provider === 'guide') && engine === 'claude') return
|
|
217
|
+
if (provider === 'codex' || engine === 'codex') return codexLabel();
|
|
218
|
+
if ((provider === 'tour' || provider === 'guide') && engine === 'claude') return claudeLabel();
|
|
237
219
|
if (provider === 'tour' || provider === 'guide') {
|
|
238
220
|
if (engine === 'cursor') return catalogLabel(CURSOR_MODELS, model);
|
|
239
221
|
if (engine === 'opencode' || engine === 'pi' || engine === 'copilot') return model || 'Default';
|
|
240
222
|
}
|
|
241
|
-
return
|
|
223
|
+
return claudeLabel();
|
|
242
224
|
}
|
|
243
225
|
|
|
244
226
|
function formatThinking(value: string): string {
|
|
@@ -247,11 +229,7 @@ function formatThinking(value: string): string {
|
|
|
247
229
|
|
|
248
230
|
|
|
249
231
|
function formatEffort(value: string): string {
|
|
250
|
-
return
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function formatReasoning(value: string): string {
|
|
254
|
-
return CODEX_EFFORT_LABELS[value] ?? value;
|
|
232
|
+
return EFFORT_LABELS[value] ?? value;
|
|
255
233
|
}
|
|
256
234
|
|
|
257
235
|
// --- Add-a-review dialog: a type-ahead picker over every discovered skill ---
|
|
@@ -383,8 +361,10 @@ function JobCard({
|
|
|
383
361
|
onToggle,
|
|
384
362
|
onViewDetails,
|
|
385
363
|
onOpenGuide,
|
|
364
|
+
catalogs,
|
|
386
365
|
}: {
|
|
387
366
|
job: AgentJobInfo;
|
|
367
|
+
catalogs: ModelLabelCatalogs;
|
|
388
368
|
annotationCount: number;
|
|
389
369
|
onKill: () => void;
|
|
390
370
|
expanded: boolean;
|
|
@@ -412,10 +392,10 @@ function JobCard({
|
|
|
412
392
|
<div className="mt-0.5 flex flex-wrap items-center gap-1 text-[9px] text-muted-foreground/50">
|
|
413
393
|
<span className="rounded bg-surface-1 px-1 py-px">{job.provider}</span>
|
|
414
394
|
{job.model && (
|
|
415
|
-
<span className="rounded bg-surface-1 px-1 py-px font-mono">{formatModel(job.provider, job.engine, job.model)}</span>
|
|
395
|
+
<span className="rounded bg-surface-1 px-1 py-px font-mono">{formatModel(job.provider, job.engine, job.model, catalogs)}</span>
|
|
416
396
|
)}
|
|
417
397
|
{job.effort && <span className="rounded bg-surface-1 px-1 py-px">{formatEffort(job.effort)}</span>}
|
|
418
|
-
{job.reasoningEffort && <span className="rounded bg-surface-1 px-1 py-px">{
|
|
398
|
+
{job.reasoningEffort && <span className="rounded bg-surface-1 px-1 py-px">{formatEffort(job.reasoningEffort)}</span>}
|
|
419
399
|
{job.thinking && <span className="rounded bg-surface-1 px-1 py-px">{formatThinking(job.thinking)}</span>}
|
|
420
400
|
{job.fastMode && (
|
|
421
401
|
<span className="rounded bg-amber-500/10 px-1 py-px text-amber-600 dark:text-amber-400">
|
|
@@ -521,7 +501,11 @@ export const AgentsTab: React.FC<AgentsTabProps> = ({
|
|
|
521
501
|
const [pendingLaunch, setPendingLaunch] = useState<{ label: string; provider?: string; startedAt: number } | null>(null);
|
|
522
502
|
const [launchError, setLaunchError] = useState<string | null>(null);
|
|
523
503
|
const launchingRef = useRef(false);
|
|
524
|
-
|
|
504
|
+
// Claude/Codex catalogs are shared with Ask AI; only the engine this panel
|
|
505
|
+
// is set to is fetched (never on page load), and saved picks resolve
|
|
506
|
+
// against it.
|
|
507
|
+
const catalogs = useModelCatalogs(capabilities);
|
|
508
|
+
const settings = useAgentSettings(catalogs);
|
|
525
509
|
const {
|
|
526
510
|
selectedMode,
|
|
527
511
|
reviewEngine,
|
|
@@ -582,6 +566,9 @@ export const AgentsTab: React.FC<AgentsTabProps> = ({
|
|
|
582
566
|
setGuidePiThinking,
|
|
583
567
|
setGuideCopilotModel,
|
|
584
568
|
} = settings;
|
|
569
|
+
const activeEngine = selectedMode === 'review' ? reviewEngine : selectedMode === 'tour' ? tourEngine : guideEngine;
|
|
570
|
+
const loadCatalog = catalogs.load;
|
|
571
|
+
useEffect(() => loadCatalog(activeEngine), [loadCatalog, activeEngine]);
|
|
585
572
|
|
|
586
573
|
// Review profiles (built-in default plus the user's enabled skills). Loaded
|
|
587
574
|
// from the discovery endpoint and refreshed after a skill is added.
|
|
@@ -923,12 +910,16 @@ export const AgentsTab: React.FC<AgentsTabProps> = ({
|
|
|
923
910
|
// the saved id can't be found yet and the launch would quietly run Default. A
|
|
924
911
|
// Default pick has nothing to resolve, so it never waits.
|
|
925
912
|
const reviewReady = profilesLoaded || reviewProfileId === 'builtin:default';
|
|
913
|
+
// Claude/Codex launches wait for the model catalog to settle (it always
|
|
914
|
+
// does — the fallback on failure), so a launch never uses an unresolved pick.
|
|
915
|
+
const modelsReady = (engine: ReviewEngine) =>
|
|
916
|
+
engine === 'claude' || engine === 'codex' ? catalogs[engine].settled : true;
|
|
926
917
|
const canLaunch = selectedMode === 'review'
|
|
927
|
-
? reviewEngineAvailable(reviewEngine) && reviewReady
|
|
918
|
+
? reviewEngineAvailable(reviewEngine) && reviewReady && modelsReady(reviewEngine)
|
|
928
919
|
: selectedMode === 'tour'
|
|
929
|
-
? tourAvailable && engineAvailable(tourEngine)
|
|
920
|
+
? tourAvailable && engineAvailable(tourEngine) && modelsReady(tourEngine)
|
|
930
921
|
: selectedMode === 'guide'
|
|
931
|
-
? guideAvailable && reviewEngineAvailable(guideEngine) && guideLaunchable
|
|
922
|
+
? guideAvailable && reviewEngineAvailable(guideEngine) && guideLaunchable && modelsReady(guideEngine)
|
|
932
923
|
: false;
|
|
933
924
|
|
|
934
925
|
const handleLaunch = async () => {
|
|
@@ -1015,6 +1006,40 @@ export const AgentsTab: React.FC<AgentsTabProps> = ({
|
|
|
1015
1006
|
);
|
|
1016
1007
|
}
|
|
1017
1008
|
|
|
1009
|
+
// Model picker for Claude and Codex, or a quiet loading row while that
|
|
1010
|
+
// engine's catalog is still settling (launch is held until it does).
|
|
1011
|
+
const renderCatalogModelPicker = (
|
|
1012
|
+
engine: 'claude' | 'codex',
|
|
1013
|
+
value: string,
|
|
1014
|
+
onChange: (value: string) => void,
|
|
1015
|
+
) => (
|
|
1016
|
+
<ConfigRow label="Model" stacked>
|
|
1017
|
+
{catalogs[engine].settled ? (
|
|
1018
|
+
<SelectMenu value={value} options={modelSelectOptions(catalogs[engine].models, value)} onChange={onChange} />
|
|
1019
|
+
) : (
|
|
1020
|
+
renderStaticChoice('Loading models…', <Loader2 className="animate-spin text-muted-foreground" size={11} />)
|
|
1021
|
+
)}
|
|
1022
|
+
</ConfigRow>
|
|
1023
|
+
);
|
|
1024
|
+
|
|
1025
|
+
// Effort / reasoning picker for Claude and Codex: only the levels the
|
|
1026
|
+
// selected model accepts, and no picker at all for a model that takes none.
|
|
1027
|
+
const renderEffortPicker = (
|
|
1028
|
+
label: string,
|
|
1029
|
+
models: readonly CatalogModel[],
|
|
1030
|
+
model: string,
|
|
1031
|
+
value: string,
|
|
1032
|
+
onChange: (value: string) => void,
|
|
1033
|
+
) => {
|
|
1034
|
+
const options = effortSelectOptions(models, model);
|
|
1035
|
+
if (options.length === 0) return null;
|
|
1036
|
+
return (
|
|
1037
|
+
<ConfigRow label={label} stacked>
|
|
1038
|
+
<SegmentedPicker options={options} value={value} onChange={onChange} />
|
|
1039
|
+
</ConfigRow>
|
|
1040
|
+
);
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1018
1043
|
// Cursor and OpenCode share the same review config: an "experimental" note and
|
|
1019
1044
|
// a single model picker driven by their live (or fallback) catalog.
|
|
1020
1045
|
const renderMarkerEngineConfig = (
|
|
@@ -1084,25 +1109,19 @@ export const AgentsTab: React.FC<AgentsTabProps> = ({
|
|
|
1084
1109
|
</ConfigRow>
|
|
1085
1110
|
{reviewEngine === 'claude' && (
|
|
1086
1111
|
<>
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
</ConfigRow>
|
|
1090
|
-
<ConfigRow label="Effort" stacked>
|
|
1091
|
-
<SegmentedPicker options={CLAUDE_EFFORT} value={claudeEffort} onChange={setClaudeEffort} />
|
|
1092
|
-
</ConfigRow>
|
|
1112
|
+
{renderCatalogModelPicker('claude', claudeModel, setClaudeModel)}
|
|
1113
|
+
{renderEffortPicker('Effort', catalogs.claude.models, claudeModel, claudeEffort, setClaudeEffort)}
|
|
1093
1114
|
</>
|
|
1094
1115
|
)}
|
|
1095
1116
|
{reviewEngine === 'codex' && (
|
|
1096
1117
|
<>
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
<Toggle checked={codexFast} onChange={setCodexFast} />
|
|
1105
|
-
</ConfigRow>
|
|
1118
|
+
{renderCatalogModelPicker('codex', codexModel, setCodexModel)}
|
|
1119
|
+
{renderEffortPicker('Reasoning', catalogs.codex.models, codexModel, codexReasoning, setCodexReasoning)}
|
|
1120
|
+
{modelSupportsFast(catalogs.codex.models, codexModel) && (
|
|
1121
|
+
<ConfigRow label="Fast mode">
|
|
1122
|
+
<Toggle checked={codexFast} onChange={setCodexFast} />
|
|
1123
|
+
</ConfigRow>
|
|
1124
|
+
)}
|
|
1106
1125
|
</>
|
|
1107
1126
|
)}
|
|
1108
1127
|
{reviewEngine === 'cursor' && renderMarkerEngineConfig(cursorModel, cursorModels, setCursorModel)}
|
|
@@ -1122,30 +1141,23 @@ export const AgentsTab: React.FC<AgentsTabProps> = ({
|
|
|
1122
1141
|
{selectedMode === 'tour' && (
|
|
1123
1142
|
<>
|
|
1124
1143
|
{renderEngineSelect(tourEngine, setTourEngine, availableEngines, ENGINE_ICON, ENGINE_LABEL)}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
options={tourEngine === 'claude' ? TOUR_CLAUDE_MODELS : CODEX_MODELS}
|
|
1129
|
-
onChange={tourEngine === 'claude' ? setTourClaudeModel : setTourCodexModel}
|
|
1130
|
-
/>
|
|
1131
|
-
</ConfigRow>
|
|
1144
|
+
{tourEngine === 'claude'
|
|
1145
|
+
? renderCatalogModelPicker('claude', tourClaudeModel, setTourClaudeModel)
|
|
1146
|
+
: renderCatalogModelPicker('codex', tourCodexModel, setTourCodexModel)}
|
|
1132
1147
|
|
|
1133
1148
|
{/* Claude-only: effort level */}
|
|
1134
|
-
{tourEngine === 'claude' &&
|
|
1135
|
-
|
|
1136
|
-
<SegmentedPicker options={CLAUDE_EFFORT} value={tourClaudeEffort} onChange={setTourClaudeEffort} />
|
|
1137
|
-
</ConfigRow>
|
|
1138
|
-
)}
|
|
1149
|
+
{tourEngine === 'claude' &&
|
|
1150
|
+
renderEffortPicker('Effort', catalogs.claude.models, tourClaudeModel, tourClaudeEffort, setTourClaudeEffort)}
|
|
1139
1151
|
|
|
1140
1152
|
{/* Codex-only: reasoning effort + fast mode */}
|
|
1141
1153
|
{tourEngine === 'codex' && (
|
|
1142
1154
|
<>
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1155
|
+
{renderEffortPicker('Reasoning', catalogs.codex.models, tourCodexModel, tourCodexReasoning, setTourCodexReasoning)}
|
|
1156
|
+
{modelSupportsFast(catalogs.codex.models, tourCodexModel) && (
|
|
1157
|
+
<ConfigRow label="Fast mode">
|
|
1158
|
+
<Toggle checked={tourCodexFast} onChange={setTourCodexFast} />
|
|
1159
|
+
</ConfigRow>
|
|
1160
|
+
)}
|
|
1149
1161
|
</>
|
|
1150
1162
|
)}
|
|
1151
1163
|
</>
|
|
@@ -1154,31 +1166,18 @@ export const AgentsTab: React.FC<AgentsTabProps> = ({
|
|
|
1154
1166
|
{selectedMode === 'guide' && (
|
|
1155
1167
|
<>
|
|
1156
1168
|
{renderEngineSelect(guideEngine, setGuideEngine, availableReviewEngines, REVIEW_ENGINE_ICON, REVIEW_ENGINE_LABEL)}
|
|
1157
|
-
{
|
|
1158
|
-
|
|
1159
|
-
<SelectMenu
|
|
1160
|
-
value={guideEngine === 'claude' ? guideClaudeModel : guideCodexModel}
|
|
1161
|
-
options={guideEngine === 'claude' ? TOUR_CLAUDE_MODELS : CODEX_MODELS}
|
|
1162
|
-
onChange={guideEngine === 'claude' ? setGuideClaudeModel : setGuideCodexModel}
|
|
1163
|
-
/>
|
|
1164
|
-
</ConfigRow>
|
|
1165
|
-
)}
|
|
1169
|
+
{guideEngine === 'claude' && renderCatalogModelPicker('claude', guideClaudeModel, setGuideClaudeModel)}
|
|
1170
|
+
{guideEngine === 'codex' && renderCatalogModelPicker('codex', guideCodexModel, setGuideCodexModel)}
|
|
1166
1171
|
|
|
1167
1172
|
{/* Claude-only: effort level */}
|
|
1168
|
-
{guideEngine === 'claude' &&
|
|
1169
|
-
|
|
1170
|
-
<SegmentedPicker options={CLAUDE_EFFORT} value={guideClaudeEffort} onChange={setGuideClaudeEffort} />
|
|
1171
|
-
</ConfigRow>
|
|
1172
|
-
)}
|
|
1173
|
+
{guideEngine === 'claude' &&
|
|
1174
|
+
renderEffortPicker('Effort', catalogs.claude.models, guideClaudeModel, guideClaudeEffort, setGuideClaudeEffort)}
|
|
1173
1175
|
|
|
1174
1176
|
{/* Codex-only: reasoning effort. No "Fast mode" toggle here
|
|
1175
1177
|
(unlike review/tour's codex blocks above) — fast mode is
|
|
1176
1178
|
deliberately not offered for guide. */}
|
|
1177
|
-
{guideEngine === 'codex' &&
|
|
1178
|
-
|
|
1179
|
-
<SegmentedPicker options={codexReasoningOptions(guideCodexModel)} value={guideCodexReasoning} onChange={setGuideCodexReasoning} />
|
|
1180
|
-
</ConfigRow>
|
|
1181
|
-
)}
|
|
1179
|
+
{guideEngine === 'codex' &&
|
|
1180
|
+
renderEffortPicker('Reasoning', catalogs.codex.models, guideCodexModel, guideCodexReasoning, setGuideCodexReasoning)}
|
|
1182
1181
|
|
|
1183
1182
|
{/* Marker engines: same live-catalog model picker as review mode,
|
|
1184
1183
|
but bound to the guide-scoped settings (see useAgentSettings) so
|
|
@@ -1237,6 +1236,7 @@ export const AgentsTab: React.FC<AgentsTabProps> = ({
|
|
|
1237
1236
|
<JobCard
|
|
1238
1237
|
key={job.id}
|
|
1239
1238
|
job={job}
|
|
1239
|
+
catalogs={catalogs}
|
|
1240
1240
|
annotationCount={annotationCounts.get(job.source) ?? 0}
|
|
1241
1241
|
onKill={() => onKillJob(job.id)}
|
|
1242
1242
|
expanded={expandedJobId === job.id}
|
|
@@ -19,9 +19,10 @@ export const BlockRenderer: React.FC<{
|
|
|
19
19
|
checkboxOverrides?: Map<string, boolean>;
|
|
20
20
|
orderedIndex?: number | null;
|
|
21
21
|
githubRepo?: string;
|
|
22
|
+
repoHost?: string;
|
|
22
23
|
headingAnchorId?: string;
|
|
23
24
|
onNavigateAnchor?: (hash: string) => void;
|
|
24
|
-
}> = ({ block, onOpenLinkedDoc, onOpenCodeFile, imageBaseDir, onImageClick, onToggleCheckbox, checkboxOverrides, orderedIndex, githubRepo, headingAnchorId, onNavigateAnchor }) => {
|
|
25
|
+
}> = ({ block, onOpenLinkedDoc, onOpenCodeFile, imageBaseDir, onImageClick, onToggleCheckbox, checkboxOverrides, orderedIndex, githubRepo, repoHost, headingAnchorId, onNavigateAnchor }) => {
|
|
25
26
|
switch (block.type) {
|
|
26
27
|
case 'heading': {
|
|
27
28
|
const Tag = `h${block.level || 1}` as React.ElementType;
|
|
@@ -37,7 +38,7 @@ export const BlockRenderer: React.FC<{
|
|
|
37
38
|
data-block-id={block.id}
|
|
38
39
|
data-block-type="heading"
|
|
39
40
|
>
|
|
40
|
-
<InlineMarkdown imageBaseDir={imageBaseDir} onImageClick={onImageClick} text={block.content} onOpenLinkedDoc={onOpenLinkedDoc} onOpenCodeFile={onOpenCodeFile} githubRepo={githubRepo} onNavigateAnchor={onNavigateAnchor} />
|
|
41
|
+
<InlineMarkdown imageBaseDir={imageBaseDir} onImageClick={onImageClick} text={block.content} onOpenLinkedDoc={onOpenLinkedDoc} onOpenCodeFile={onOpenCodeFile} githubRepo={githubRepo} repoHost={repoHost} onNavigateAnchor={onNavigateAnchor} />
|
|
41
42
|
</Tag>
|
|
42
43
|
);
|
|
43
44
|
}
|
|
@@ -54,6 +55,7 @@ export const BlockRenderer: React.FC<{
|
|
|
54
55
|
imageBaseDir={imageBaseDir}
|
|
55
56
|
onImageClick={onImageClick}
|
|
56
57
|
githubRepo={githubRepo}
|
|
58
|
+
repoHost={repoHost}
|
|
57
59
|
onNavigateAnchor={onNavigateAnchor}
|
|
58
60
|
/>
|
|
59
61
|
);
|
|
@@ -68,7 +70,7 @@ export const BlockRenderer: React.FC<{
|
|
|
68
70
|
>
|
|
69
71
|
{paragraphs.map((para, i) => (
|
|
70
72
|
<p key={i} className={i > 0 ? 'mt-2' : ''}>
|
|
71
|
-
<InlineMarkdown imageBaseDir={imageBaseDir} onImageClick={onImageClick} text={para} onOpenLinkedDoc={onOpenLinkedDoc} onOpenCodeFile={onOpenCodeFile} githubRepo={githubRepo} onNavigateAnchor={onNavigateAnchor} />
|
|
73
|
+
<InlineMarkdown imageBaseDir={imageBaseDir} onImageClick={onImageClick} text={para} onOpenLinkedDoc={onOpenLinkedDoc} onOpenCodeFile={onOpenCodeFile} githubRepo={githubRepo} repoHost={repoHost} onNavigateAnchor={onNavigateAnchor} />
|
|
72
74
|
</p>
|
|
73
75
|
))}
|
|
74
76
|
</blockquote>
|
|
@@ -83,7 +85,7 @@ export const BlockRenderer: React.FC<{
|
|
|
83
85
|
: block.checked;
|
|
84
86
|
const isInteractive = isCheckbox && !!onToggleCheckbox;
|
|
85
87
|
const textClass = `text-sm leading-relaxed ${isCheckbox && isChecked ? 'text-muted-foreground line-through' : 'text-foreground/90'}`;
|
|
86
|
-
const inlineProps = { imageBaseDir, onImageClick, onOpenLinkedDoc, onOpenCodeFile, githubRepo, onNavigateAnchor };
|
|
88
|
+
const inlineProps = { imageBaseDir, onImageClick, onOpenLinkedDoc, onOpenCodeFile, githubRepo, repoHost, onNavigateAnchor };
|
|
87
89
|
return (
|
|
88
90
|
<div
|
|
89
91
|
className="flex items-start gap-3 my-1.5"
|
|
@@ -117,6 +119,7 @@ export const BlockRenderer: React.FC<{
|
|
|
117
119
|
onOpenLinkedDoc={onOpenLinkedDoc}
|
|
118
120
|
onOpenCodeFile={onOpenCodeFile}
|
|
119
121
|
githubRepo={githubRepo}
|
|
122
|
+
repoHost={repoHost}
|
|
120
123
|
onNavigateAnchor={onNavigateAnchor}
|
|
121
124
|
/>
|
|
122
125
|
);
|
|
@@ -145,6 +148,7 @@ export const BlockRenderer: React.FC<{
|
|
|
145
148
|
imageBaseDir={imageBaseDir}
|
|
146
149
|
onImageClick={onImageClick}
|
|
147
150
|
githubRepo={githubRepo}
|
|
151
|
+
repoHost={repoHost}
|
|
148
152
|
onNavigateAnchor={onNavigateAnchor}
|
|
149
153
|
/>
|
|
150
154
|
);
|
|
@@ -156,7 +160,7 @@ export const BlockRenderer: React.FC<{
|
|
|
156
160
|
className="mb-4 leading-relaxed text-foreground/90 text-[15px]"
|
|
157
161
|
data-block-id={block.id}
|
|
158
162
|
>
|
|
159
|
-
<InlineMarkdown imageBaseDir={imageBaseDir} onImageClick={onImageClick} text={block.content} onOpenLinkedDoc={onOpenLinkedDoc} onOpenCodeFile={onOpenCodeFile} githubRepo={githubRepo} onNavigateAnchor={onNavigateAnchor} />
|
|
163
|
+
<InlineMarkdown imageBaseDir={imageBaseDir} onImageClick={onImageClick} text={block.content} onOpenLinkedDoc={onOpenLinkedDoc} onOpenCodeFile={onOpenCodeFile} githubRepo={githubRepo} repoHost={repoHost} onNavigateAnchor={onNavigateAnchor} />
|
|
160
164
|
</p>
|
|
161
165
|
);
|
|
162
166
|
}
|
|
@@ -136,7 +136,7 @@ export const ExportModal: React.FC<ExportModalProps> = ({
|
|
|
136
136
|
await handleCopy(wrapCopiedAnnotations(annotationsOutput), 'annotations');
|
|
137
137
|
};
|
|
138
138
|
|
|
139
|
-
//
|
|
139
|
+
// Warn when the hash URL may be too long for messaging apps
|
|
140
140
|
const urlIsLarge = shareUrl.length > 2048;
|
|
141
141
|
// Hash-based sharing unavailable (e.g. HTML render mode) — show only short link
|
|
142
142
|
const hashUnavailable = !shareUrl && !!onGenerateShortUrl;
|
|
@@ -327,9 +327,13 @@ export const ExportModal: React.FC<ExportModalProps> = ({
|
|
|
327
327
|
</svg>
|
|
328
328
|
Generating short link...
|
|
329
329
|
</div>
|
|
330
|
-
) :
|
|
331
|
-
<div className=
|
|
332
|
-
|
|
330
|
+
) : onGenerateShortUrl ? (
|
|
331
|
+
<div className={`p-3 rounded-lg border ${
|
|
332
|
+
urlIsLarge
|
|
333
|
+
? 'bg-amber-500/10 border-amber-500/20'
|
|
334
|
+
: 'bg-muted/50 border-border'
|
|
335
|
+
}`}>
|
|
336
|
+
{urlIsLarge && (
|
|
333
337
|
<p className="text-xs text-amber-600 dark:text-amber-400 mb-2">
|
|
334
338
|
This URL may be too long for some messaging apps.
|
|
335
339
|
</p>
|
|
@@ -104,7 +104,15 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
|
|
104
104
|
const canIncrease = currentSizeIndex < STROKE_SIZES.length - 1;
|
|
105
105
|
|
|
106
106
|
return (
|
|
107
|
-
<div
|
|
107
|
+
<div
|
|
108
|
+
data-pn-image-annotator-toolbar
|
|
109
|
+
className="flex w-fit max-w-full items-center gap-2 px-3 py-2 bg-popover border border-border rounded-lg shadow-xl"
|
|
110
|
+
>
|
|
111
|
+
<div
|
|
112
|
+
data-pn-image-toolbar-scroll
|
|
113
|
+
className="min-w-0 flex-1 overflow-x-auto overscroll-x-contain"
|
|
114
|
+
>
|
|
115
|
+
<div className="flex w-max items-center gap-2">
|
|
108
116
|
{/* Tools */}
|
|
109
117
|
<div className="flex items-center gap-1">
|
|
110
118
|
{TOOLS.map(({ id, icon: Icon, label }) => (
|
|
@@ -228,15 +236,17 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
|
|
228
236
|
>
|
|
229
237
|
<ClearIcon />
|
|
230
238
|
</button>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
231
241
|
|
|
232
|
-
<div className="w-px h-5 bg-border" />
|
|
242
|
+
<div className="w-px h-5 shrink-0 bg-border" />
|
|
233
243
|
|
|
234
244
|
{/* Save */}
|
|
235
245
|
<button
|
|
236
246
|
type="button"
|
|
237
247
|
onClick={onSave}
|
|
238
248
|
title="Save (Esc)"
|
|
239
|
-
className="p-1.5 rounded transition-colors bg-success text-success-foreground hover:opacity-90"
|
|
249
|
+
className="shrink-0 p-1.5 rounded transition-colors bg-success text-success-foreground hover:opacity-90"
|
|
240
250
|
>
|
|
241
251
|
<CheckIcon />
|
|
242
252
|
</button>
|
|
@@ -185,12 +185,16 @@ export const ImageAnnotator: React.FC<ImageAnnotatorProps> = ({
|
|
|
185
185
|
|
|
186
186
|
return (
|
|
187
187
|
<div
|
|
188
|
-
className="
|
|
188
|
+
className="pn-visible-viewport-overlay z-[200] flex items-center justify-center bg-background/90 backdrop-blur-sm"
|
|
189
189
|
data-popover-layer
|
|
190
190
|
onClick={handleBackdropClick}
|
|
191
191
|
>
|
|
192
192
|
{/* Canvas with image and toolbar */}
|
|
193
|
-
<div
|
|
193
|
+
<div
|
|
194
|
+
data-pn-image-annotator-stage
|
|
195
|
+
className="relative flex max-w-full flex-col items-center gap-3"
|
|
196
|
+
onClick={(e) => e.stopPropagation()}
|
|
197
|
+
>
|
|
194
198
|
{/* Toolbar - above image */}
|
|
195
199
|
<Toolbar
|
|
196
200
|
tool={state.tool}
|