@plannotator/ui 0.51.0 → 0.52.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 +235 -0
- package/README.md +15 -0
- package/components/AnnotationToolbar.tsx +2 -2
- package/components/BlockRenderer.tsx +16 -1
- package/components/CommentPopover.tsx +4 -0
- package/components/CompletionOverlay.tsx +12 -5
- package/components/FloatingQuickLabelPicker.tsx +2 -2
- package/components/QuickLabelDropdown.tsx +9 -4
- package/components/ServerSessionReplacedBanner.tsx +29 -0
- package/components/Settings.tsx +3 -191
- package/components/Viewer.tsx +44 -1
- package/components/blocks/QuestionBlock.tsx +137 -8
- package/components/html-viewer/bridge-script.asset.js +106 -6
- package/components/html-viewer/bridge-script.ts +106 -6
- package/components/settings/QuickLabelsTab.tsx +378 -0
- package/hooks/useAIChat.ts +57 -1
- package/hooks/useAnnotationHighlighter.ts +28 -0
- package/package.json +2 -2
- package/styles.css +1 -1
- package/types.ts +6 -0
- package/utils/parser.ts +149 -7
- package/utils/quickLabels.ts +80 -0
- package/utils/serverSession.ts +85 -0
package/HANDOFF.md
CHANGED
|
@@ -1880,6 +1880,241 @@ Pinned by `components/DiagramBlock.askAI.test.tsx` and
|
|
|
1880
1880
|
|
|
1881
1881
|
---
|
|
1882
1882
|
|
|
1883
|
+
## Ask AI names a text selection's source lines (ui 0.52.0; additive, no core change)
|
|
1884
|
+
|
|
1885
|
+
An Ask AI question asked from a markdown text selection now says which
|
|
1886
|
+
source lines the selection is on, so a phrase that occurs more than once in
|
|
1887
|
+
the document is no longer ambiguous (#1731). Nothing to pass; a host that
|
|
1888
|
+
ignores the new fields keeps working.
|
|
1889
|
+
|
|
1890
|
+
- **`CommentAskAIContext.lineStart?` / `lineEnd?`** and
|
|
1891
|
+
**`AIQuestion.scope.lineStart?` / `lineEnd?`** (1-based, additive). `Viewer`
|
|
1892
|
+
fills them on the context its text-selection composer and its code-block
|
|
1893
|
+
composer hand to `onAskAI`: the span of the enclosing block, which for a
|
|
1894
|
+
selection within one block is the same span the exported annotation from
|
|
1895
|
+
that selection prints as `(line 41)` / `(lines 41–44)`. A selection over
|
|
1896
|
+
several blocks gives a range from the first block's start line to the last
|
|
1897
|
+
block's end line, while the export names only the first block. The global
|
|
1898
|
+
composer, HTML / live-app
|
|
1899
|
+
pinpoints and diagram comments carry none (diagrams already send their line
|
|
1900
|
+
in `detail`).
|
|
1901
|
+
- **`buildDefaultPrompt`** appends them to the `Source:` line:
|
|
1902
|
+
`Source: /docs/plan.md, line 41` or `Source: /docs/plan.md, lines 41–44`
|
|
1903
|
+
(`Source: line 41` with no `sourcePath`). A scope without them, or with a
|
|
1904
|
+
value that is not a positive integer, prints exactly what it printed before.
|
|
1905
|
+
A host that maps `CommentAskAIContext` onto `AIQuestion.scope` field by
|
|
1906
|
+
field must copy the two fields to get the line into the question.
|
|
1907
|
+
- **New pure helper `selectionSourceLines(blocks, blockIds)`** (`utils/parser`)
|
|
1908
|
+
returns `{ lineStart, lineEnd }` or `null`.
|
|
1909
|
+
- **`CommentPopoverState.blockIds?`** (`hooks/useAnnotationHighlighter`,
|
|
1910
|
+
additive): the `data-block-id`s the pending selection covers, which is what
|
|
1911
|
+
`Viewer` maps to lines.
|
|
1912
|
+
|
|
1913
|
+
Lines are document lines of the markdown the `Viewer` was given (frontmatter
|
|
1914
|
+
counted). Plannotator drops them for a converted HTML / URL source, whose
|
|
1915
|
+
converted markdown lines do not exist in the original file (its mapping is
|
|
1916
|
+
`packages/editor/askScope.ts`, pinned by `askScope.test.ts`). Pinned by
|
|
1917
|
+
`components/Viewer.askAISelectionLine.test.tsx` (DOM-gated),
|
|
1918
|
+
`utils/parser.selectionLines.test.ts` (including parity with the export
|
|
1919
|
+
heading for single-block selections) and `utils/aiPrompt.test.ts`.
|
|
1920
|
+
|
|
1921
|
+
---
|
|
1922
|
+
|
|
1923
|
+
## `CompletionOverlay` `done` state (ui 0.52.0; additive)
|
|
1924
|
+
|
|
1925
|
+
`submitted` accepts a new value, `'done'`: the session finished with nothing
|
|
1926
|
+
sent (Plannotator uses it for an annotate Done with nothing to send). It
|
|
1927
|
+
renders a neutral check icon; the host still supplies the title and subtitle.
|
|
1928
|
+
`ComponentProps<typeof CompletionOverlay>['submitted']` therefore now includes
|
|
1929
|
+
`'done'`, so a host that switches exhaustively over that type gets one more
|
|
1930
|
+
case to handle.
|
|
1931
|
+
|
|
1932
|
+
Neither `'done'` nor `'exited'` (Close) renders the footer's "Your response
|
|
1933
|
+
has been sent." line any more, since neither sends anything. That line only
|
|
1934
|
+
appears in the auto-close `closed` phase (an immediate auto-close whose close
|
|
1935
|
+
request went to a host window such as Glimpse or the VS Code panel's parent),
|
|
1936
|
+
so this is a visible change for `'exited'` only there. Every other value
|
|
1937
|
+
renders exactly as before. The overlay root now carries
|
|
1938
|
+
`data-completion-state={submitted}`.
|
|
1939
|
+
|
|
1940
|
+
## Quick Labels: reorder and editable emoji (ui 0.52.0; additive, no core change)
|
|
1941
|
+
|
|
1942
|
+
Settings → Labels (#1736) lets a person move a label up or down (per-row
|
|
1943
|
+
buttons, `aria-label="Move <text> up|down"`, a polite live region announces
|
|
1944
|
+
the new position and key) and change its emoji (a one-emoji field; anything
|
|
1945
|
+
that is not exactly one emoji is shown invalid, never saved, and reverts on
|
|
1946
|
+
blur). The tab now lives in `components/settings/QuickLabelsTab` and
|
|
1947
|
+
`Settings` renders it; nothing about `Settings`' props changed.
|
|
1948
|
+
|
|
1949
|
+
**The persisted shape is unchanged.** Labels are still saved as
|
|
1950
|
+
`QuickLabel[]` under `plannotator-quick-labels` through the storage backend,
|
|
1951
|
+
with the same fields; a reorder is just a different array order and an emoji
|
|
1952
|
+
edit a different `emoji` value. Rows get a stable identity for the editing
|
|
1953
|
+
session only (it is never written, so an older reader sees nothing new and the
|
|
1954
|
+
cookie does not grow). `QuickLabel.id` keeps its old meaning (derived from the
|
|
1955
|
+
text when the text is edited; `custom-<ts>` for new labels) and is still not
|
|
1956
|
+
unique: the list renderer `QuickLabelDropdown` now keys rows by position, so
|
|
1957
|
+
two labels with the same text no longer collide.
|
|
1958
|
+
|
|
1959
|
+
What a host should know:
|
|
1960
|
+
|
|
1961
|
+
- **Position is the key binding.** Alt/⌥1…9, 0 apply the label at positions
|
|
1962
|
+
1…10 (`quickLabelShortcutDigit` / `quickLabelIndexForDigit`); moving a label
|
|
1963
|
+
in Settings is what remaps its key. Lists are read once when a toolbar or
|
|
1964
|
+
picker mounts, so a change applies to the next selection.
|
|
1965
|
+
- **An annotation keeps the emoji it was created with.** The label is copied
|
|
1966
|
+
into the annotation as `${emoji} ${text}`; editing the emoji (like editing
|
|
1967
|
+
the text) affects only annotations created afterwards.
|
|
1968
|
+
- New pure exports in `utils/quickLabels`: `quickLabelShortcutDigit`,
|
|
1969
|
+
`quickLabelIndexForDigit`, `moveQuickLabel`, `parseQuickLabelEmoji` (one
|
|
1970
|
+
emoji grapheme, including ZWJ sequences, flags, keycaps and skin tones, or
|
|
1971
|
+
null) and `emojiFromFieldInput`. Nothing was removed or renamed.
|
|
1972
|
+
|
|
1973
|
+
## Question decision toggle and status tag (ui 0.52.0; additive, no core change)
|
|
1974
|
+
|
|
1975
|
+
What Workspaces asked for: the reviewer can switch a `Decision: when answered`
|
|
1976
|
+
question's recording off and on from the card itself, and the host can draw
|
|
1977
|
+
its own status tag. Three optional props on `Viewer`, forwarded through
|
|
1978
|
+
`BlockRenderer` to `QuestionBlock` the way `renderQuestionFooter` is.
|
|
1979
|
+
Plannotator passes none of them, so its cards render exactly as before.
|
|
1980
|
+
|
|
1981
|
+
| `Viewer` | `BlockRenderer` | `QuestionBlock` |
|
|
1982
|
+
| --- | --- | --- |
|
|
1983
|
+
| `questionDecisionRecording?: (question: IndexedQuestion) => boolean \| undefined` | `questionDecisionRecording?: boolean` | `decisionRecording?: boolean` |
|
|
1984
|
+
| `onToggleQuestionDecisionRecording?: (key, next) => void` | same | `onToggleDecisionRecording?: (key, next) => void` |
|
|
1985
|
+
| `questionStatusTag?: 'card' \| 'none'` | same | `statusTag?: 'card' \| 'none'` |
|
|
1986
|
+
|
|
1987
|
+
- **Recording state** (`decisionRecording`, default `true`; on `Viewer` a
|
|
1988
|
+
function of the question, called per question card, so it can read your
|
|
1989
|
+
store by `question.question.key`). It applies only to a question carrying
|
|
1990
|
+
`Decision: when answered` (`decisionOnAnswer`) whose decision is not
|
|
1991
|
+
already recorded. Off draws the same "Records a decision" tag dimmed (in
|
|
1992
|
+
the theme's `--muted-foreground`, not by opacity, so it stays as legible as
|
|
1993
|
+
any secondary text) with a dotted outline and no fill (never different
|
|
1994
|
+
words), and hides the "Answering this
|
|
1995
|
+
records a decision" row (`[data-question-decision-row]`). A recorded
|
|
1996
|
+
decision (`Decision: [statement](url)`) and a question with no decision line
|
|
1997
|
+
are never affected.
|
|
1998
|
+
- **The toggle** (`onToggleDecisionRecording`): the tag becomes
|
|
1999
|
+
`<button type="button" aria-pressed>` with `[data-question-decision-toggle]`,
|
|
2000
|
+
keyboard operable, a focus ring (a ring, not an outline: the dotted outline
|
|
2001
|
+
draws the off state). A click calls the handler with the question's key and
|
|
2002
|
+
the NEXT state; the card does not keep the state itself, so pass it back
|
|
2003
|
+
through `decisionRecording` / `questionDecisionRecording`.
|
|
2004
|
+
- **Read-only cards never offer the toggle.** A card is read-only when it has
|
|
2005
|
+
no answer handler (`Viewer`: `readOnly`, or neither `onAnswerQuestion` nor
|
|
2006
|
+
`onSaveQuestionAnswer`); it draws the tag in its current state as a plain
|
|
2007
|
+
tag. `Viewer` also drops `onToggleQuestionDecisionRecording` under
|
|
2008
|
+
`readOnly`, like the answer handlers.
|
|
2009
|
+
- **`decisionRecording: false` without a handler** is honored: the static tag
|
|
2010
|
+
is drawn off and the row is hidden. Use it to show a state the reviewer may
|
|
2011
|
+
not change.
|
|
2012
|
+
- **Attributes.** `data-question-decision` keeps its values (`on-answer`,
|
|
2013
|
+
`recorded`) whatever the toggle says, so a selector keyed on the question's
|
|
2014
|
+
kind does not move. A separate `data-question-decision-recording="on" | "off"`
|
|
2015
|
+
appears on the card only for a `decisionOnAnswer` question when the host
|
|
2016
|
+
passed `decisionRecording` or the handler; a card with neither carries no
|
|
2017
|
+
new attribute.
|
|
2018
|
+
- **`statusTag: 'none'`** hides the card's own Open / Answered / Settled /
|
|
2019
|
+
Skipped tag; the decision tags ("Decision", "Records a decision") stay, and
|
|
2020
|
+
`data-question-status` still carries the status for your own tag.
|
|
2021
|
+
- **No stylesheet change.** The toggle reuses utility classes the package
|
|
2022
|
+
already ships, and the off state is an inline style, so `styles.css` and
|
|
2023
|
+
the guides.show viewer manifest are unchanged (no guides.show deploy needed
|
|
2024
|
+
for this).
|
|
2025
|
+
|
|
2026
|
+
Pinned by `components/blocks/QuestionBlock.decisionToggle.test.tsx`
|
|
2027
|
+
(DOM-gated): the toggle's button, `aria-pressed` and handler arguments, the
|
|
2028
|
+
row hidden when off, recorded and plain questions untouched, read-only cards
|
|
2029
|
+
static, `statusTag: 'none'`, the `Viewer` threading, and a card with none of
|
|
2030
|
+
the props rendering the same markup.
|
|
2031
|
+
|
|
2032
|
+
## Decision toggle on any question, and an opener for the host's decision card (ui 0.52.1; additive, no core change)
|
|
2033
|
+
|
|
2034
|
+
Two gaps Workspaces found in 0.52.0 against its approved Inbox design
|
|
2035
|
+
("the tag is the switch" on every question, off by default; "clicking the
|
|
2036
|
+
tag's words opens the decision card"). Plannotator passes none of this, and a
|
|
2037
|
+
card without the new props renders byte-identically to 0.52.0 (checked by
|
|
2038
|
+
rendering main and the branch through `BlockRenderer` and `Viewer`,
|
|
2039
|
+
interactive and read-only, with no props, with a 0.52.0-shaped host, and with
|
|
2040
|
+
a 0.52.0 host returning a boolean for every question).
|
|
2041
|
+
|
|
2042
|
+
| `Viewer` | `BlockRenderer` | `QuestionBlock` |
|
|
2043
|
+
| --- | --- | --- |
|
|
2044
|
+
| `questionDecisionScope?: 'when-answered' \| 'any'` | same | `decisionScope?: 'when-answered' \| 'any'` |
|
|
2045
|
+
| `onOpenQuestionDecision?: (key, anchor: HTMLElement) => void` | same | `onOpenDecision?: (key, anchor: HTMLElement) => void` |
|
|
2046
|
+
|
|
2047
|
+
1. **Any question can record a decision, opt-in.** With
|
|
2048
|
+
`questionDecisionScope="any"` (`decisionScope: 'any'`),
|
|
2049
|
+
`decisionRecording` / `questionDecisionRecording` also reaches a question
|
|
2050
|
+
WITHOUT a decision line: a boolean adds the "◇ Records a decision" tag in
|
|
2051
|
+
that state (and, with `onToggleDecisionRecording`, makes it the toggle),
|
|
2052
|
+
`undefined` leaves the question as it was (no tag). The default,
|
|
2053
|
+
`'when-answered'`, is the 0.52.0 behaviour: only `Decision: when answered`
|
|
2054
|
+
questions carry the tag, and a boolean returned for a plain question is
|
|
2055
|
+
ignored, as 0.52.0 documented. It is an explicit opt-in rather than
|
|
2056
|
+
"a boolean means show it" so that a 0.52.0 host that returns a boolean for
|
|
2057
|
+
every question does not grow tags on its plain questions in a patch
|
|
2058
|
+
release. On shows the same "Answering this records
|
|
2059
|
+
a decision" row a `Decision: when answered` question shows; off hides it.
|
|
2060
|
+
The default for such a question is whatever you return: start it `false`
|
|
2061
|
+
for "off by default". A question with a recorded decision
|
|
2062
|
+
(`Decision: [statement](url)`) is still never affected, and
|
|
2063
|
+
`data-question-decision` still describes the question's own line (absent
|
|
2064
|
+
for a plain question); `data-question-decision-recording="on" | "off"`
|
|
2065
|
+
appears once you supply a state.
|
|
2066
|
+
2. **A separate opener (`onOpenDecision` / `onOpenQuestionDecision`).** Given,
|
|
2067
|
+
the tag is drawn as two adjacent targets inside one visual tag
|
|
2068
|
+
(`[data-question-decision-tag]`), siblings, never nested:
|
|
2069
|
+
- the diamond is the switch: `<button aria-pressed aria-label="Record as a
|
|
2070
|
+
decision" data-question-decision-toggle>` calling
|
|
2071
|
+
`onToggleDecisionRecording(key, next)`. Without a toggle handler the
|
|
2072
|
+
diamond is decoration, not a control.
|
|
2073
|
+
- the words "Records a decision" are `<button aria-haspopup="dialog"
|
|
2074
|
+
data-question-decision-open>`, calling `onOpenDecision(key, element)`
|
|
2075
|
+
with the words' own button as the popover anchor. The card keeps no
|
|
2076
|
+
open state; your popover owns focus and returns it to the anchor.
|
|
2077
|
+
The opener is offered whether recording is on or off: what an off
|
|
2078
|
+
question's card does (switch it on as it opens, or open a card that says
|
|
2079
|
+
it is off) is the host's call. Tab order is switch, then words, both with
|
|
2080
|
+
the `ring-2 ring-primary` focus ring the 0.52.0 toggle uses (a ring, not
|
|
2081
|
+
an outline, because the dotted outline draws the off state, which is now
|
|
2082
|
+
on the wrapping tag). The off look is unchanged: muted-foreground text and
|
|
2083
|
+
a dotted outline (#1744's contrast fix). Without `onOpenDecision` the
|
|
2084
|
+
whole tag is the toggle exactly as in 0.52.0.
|
|
2085
|
+
3. **Read-only cards are never interactive.** No answer handler (or
|
|
2086
|
+
`Viewer` `readOnly`, which also drops `onOpenQuestionDecision`) draws the
|
|
2087
|
+
tag as a plain tag in its current state.
|
|
2088
|
+
|
|
2089
|
+
Measured in headless Chromium with the default theme: switch and words are
|
|
2090
|
+
7.37:1 (dark) / 8.98:1 (light) off, 5.84:1 / 5.15:1 on; the focus ring is
|
|
2091
|
+
7.53:1 / 6.53:1 against the card. The split tag draws exactly the 0.52.0 tag
|
|
2092
|
+
(127.66 × 17.75 px, the same 4 px between diamond and words). Target size
|
|
2093
|
+
(WCAG 2.5.8) is met without changing that look: the diamond (18 × 17.75 px
|
|
2094
|
+
drawn) carries an invisible `before:` box 6 px past it on every side, so it
|
|
2095
|
+
clicks as 24 × 29.75 px; the words, positioned and later in the tree, paint
|
|
2096
|
+
over the box's right-hand part, so a click on the words is always the words,
|
|
2097
|
+
and the words (110 px wide) meet the spacing exception.
|
|
2098
|
+
|
|
2099
|
+
**No stylesheet change**: only utility classes the package already ships
|
|
2100
|
+
(the hit box is the `before:absolute before:-inset-1.5 before:content-['']`
|
|
2101
|
+
pattern of the annotation panel's icon buttons), so `styles.css` and the
|
|
2102
|
+
guides.show viewer manifest are unchanged (`check:manifest` in sync).
|
|
2103
|
+
|
|
2104
|
+
Pinned by `components/blocks/QuestionBlock.decisionAnyQuestion.test.tsx`
|
|
2105
|
+
(DOM-gated): the tag on a plain question only with the `'any'` scope and a
|
|
2106
|
+
supplied state (a boolean without the scope changes nothing), the
|
|
2107
|
+
toggle's arguments and the row on/off, the switch and the opener as sibling
|
|
2108
|
+
buttons with the anchor element, tab order, the opener without a toggle
|
|
2109
|
+
handler, 0.52.0 behavior without `onOpenDecision`, read-only cards, `Viewer`
|
|
2110
|
+
threading, and no-prop markup.
|
|
2111
|
+
|
|
2112
|
+
---
|
|
2113
|
+
|
|
2114
|
+
## Stale-tab guard: `serverSession` (ui 0.52.0, core 0.25.12; additive)
|
|
2115
|
+
|
|
2116
|
+
Plannotator's servers issue one random `serverSession` nonce per process (new `@plannotator/core/server-session`) and refuse a decision that echoes a different one with `409 { code: "session_mismatch" }`, so a tab left open on a reused port cannot decide a new session. The client half is `utils/serverSession` (`adoptServerSession`, `withServerSession`, `withServerSessionQuery`, `noteServerSessionMismatch`, `useServerSessionReplaced`) plus `components/ServerSessionReplacedBanner`. Hosts that post decisions to their own backend need none of it: nothing in the shared components sends the field on its own, and a request without it is always accepted.
|
|
2117
|
+
|
|
1883
2118
|
## The law (guardrails for anyone editing `@plannotator/ui`)
|
|
1884
2119
|
|
|
1885
2120
|
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
|
@@ -384,6 +384,21 @@ See HANDOFF.md § "Questions in documents (0.48.0, core 0.25.8)".
|
|
|
384
384
|
|
|
385
385
|
See HANDOFF.md § "Question host seams (ui 0.49.0, core 0.25.9)".
|
|
386
386
|
|
|
387
|
+
**Decision recording toggle and status tag (ui 0.52.0, unreleased).** Also optional on `Viewer`, also unused by Plannotator:
|
|
388
|
+
|
|
389
|
+
- `questionDecisionRecording(question) => boolean | undefined`: false draws a `Decision: when answered` question's "Records a decision" tag dimmed with a dotted outline and hides its "Answering this records a decision" row. A recorded decision is never affected.
|
|
390
|
+
- `onToggleQuestionDecisionRecording(key, next)`: makes that tag an `aria-pressed` toggle button; store `next` and return it from `questionDecisionRecording`. Not offered under `readOnly` or in a card without an answer handler.
|
|
391
|
+
- `questionStatusTag: 'none'`: hides the card's own Open / Answered / Settled / Skipped tag so you can draw your own; the decision tags stay.
|
|
392
|
+
|
|
393
|
+
See HANDOFF.md § "Question decision toggle and status tag (ui 0.52.0, unreleased; …)".
|
|
394
|
+
|
|
395
|
+
**Decision toggle on any question, and a decision-card opener (ui 0.52.1, unreleased).** Also optional on `Viewer`, also unused by Plannotator:
|
|
396
|
+
|
|
397
|
+
- `questionDecisionScope?: 'when-answered' | 'any'` (default `'when-answered'`, the 0.52.0 behaviour). With `'any'`, `questionDecisionRecording(question)` also applies to a question with no decision line: a boolean adds the "Records a decision" tag in that state (true also shows the "Answering this records a decision" row); `undefined` leaves it tag-less. Return `false` for "off by default".
|
|
398
|
+
- `onOpenQuestionDecision(key, anchor)`: splits the tag into a diamond switch (`aria-pressed`, "Record as a decision", driven by `onToggleQuestionDecisionRecording`) and the words as a button (`aria-haspopup="dialog"`) that calls this with its own element to anchor your decision card. Offered on and off; not offered under `readOnly`. Without it the whole tag is the toggle, as in 0.52.0.
|
|
399
|
+
|
|
400
|
+
See HANDOFF.md § "Decision toggle on any question, and an opener for the host's decision card (ui 0.52.1, …)".
|
|
401
|
+
|
|
387
402
|
### Diff file tree (`components/DiffFileTree`; next ui release)
|
|
388
403
|
|
|
389
404
|
A read-only file tree for a list of changed files, to sit beside a host's own per-file diff list. It is Plannotator's code-review tree, not a lookalike: the builder (`utils/diffFileTree`), the folder open/closed hook (`hooks/useDiffFileTreeExpansion`) and the row atoms moved here from `packages/review-editor`, and the review now renders from them.
|
|
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef, useMemo } from "react";
|
|
|
2
2
|
import { AnnotationType } from "../types";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
4
|
import { useDismissOnOutsideAndEscape } from "../hooks/useDismissOnOutsideAndEscape";
|
|
5
|
-
import { type QuickLabel, getQuickLabels, THUMBS_UP_LABEL } from "../utils/quickLabels";
|
|
5
|
+
import { type QuickLabel, getQuickLabels, quickLabelIndexForDigit, THUMBS_UP_LABEL } from "../utils/quickLabels";
|
|
6
6
|
import { copyTextToClipboard } from "../utils/clipboard";
|
|
7
7
|
import { acquireTypeToCommentCapture } from "../shortcuts/plan-review/annotationMode.shortcuts";
|
|
8
8
|
import { FloatingQuickLabelPicker } from "./FloatingQuickLabelPicker";
|
|
@@ -174,7 +174,7 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
|
|
|
174
174
|
e.preventDefault();
|
|
175
175
|
if (!commentOnly && quickLabelsEnabled) {
|
|
176
176
|
const digit = parseInt(e.code.slice(5), 10);
|
|
177
|
-
const index = digit
|
|
177
|
+
const index = quickLabelIndexForDigit(digit);
|
|
178
178
|
if (index < quickLabels.length) {
|
|
179
179
|
onQuickLabel?.(quickLabels[index]);
|
|
180
180
|
}
|
|
@@ -51,7 +51,17 @@ export const BlockRenderer: React.FC<{
|
|
|
51
51
|
saveQuestionAnswerLabel?: QuestionBlockProps['saveLabel'];
|
|
52
52
|
/** Host actions in a question card's footer (see `QuestionBlock`). */
|
|
53
53
|
renderQuestionFooter?: QuestionBlockProps['renderFooter'];
|
|
54
|
-
|
|
54
|
+
/** Whether answering this question records a decision (see `QuestionBlock`). */
|
|
55
|
+
questionDecisionRecording?: QuestionBlockProps['decisionRecording'];
|
|
56
|
+
/** `'any'` lets a question without a decision line carry the tag (see `QuestionBlock`). */
|
|
57
|
+
questionDecisionScope?: QuestionBlockProps['decisionScope'];
|
|
58
|
+
/** Makes the "Records a decision" tag a toggle (see `QuestionBlock`). */
|
|
59
|
+
onToggleQuestionDecisionRecording?: QuestionBlockProps['onToggleDecisionRecording'];
|
|
60
|
+
/** Opens the host's decision card from the tag's words (see `QuestionBlock`). */
|
|
61
|
+
onOpenQuestionDecision?: QuestionBlockProps['onOpenDecision'];
|
|
62
|
+
/** `'none'` hides the card's own status tag (see `QuestionBlock`). */
|
|
63
|
+
questionStatusTag?: QuestionBlockProps['statusTag'];
|
|
64
|
+
}> = ({ block, onOpenLinkedDoc, onOpenCodeFile, imageBaseDir, onImageClick, onToggleCheckbox, checkboxOverrides, orderedIndex, githubRepo, repoHost, headingAnchorId, onNavigateAnchor, question, questionTotal, questionAnswer, onAnswerQuestion, onSaveQuestionAnswer, saveQuestionAnswerLabel, renderQuestionFooter, questionDecisionRecording, questionDecisionScope, onToggleQuestionDecisionRecording, onOpenQuestionDecision, questionStatusTag }) => {
|
|
55
65
|
switch (block.type) {
|
|
56
66
|
case 'heading': {
|
|
57
67
|
const Tag = `h${block.level || 1}` as React.ElementType;
|
|
@@ -178,6 +188,11 @@ export const BlockRenderer: React.FC<{
|
|
|
178
188
|
onSaveAnswer={onSaveQuestionAnswer}
|
|
179
189
|
saveLabel={saveQuestionAnswerLabel}
|
|
180
190
|
renderFooter={renderQuestionFooter}
|
|
191
|
+
decisionRecording={questionDecisionRecording}
|
|
192
|
+
decisionScope={questionDecisionScope}
|
|
193
|
+
onToggleDecisionRecording={onToggleQuestionDecisionRecording}
|
|
194
|
+
onOpenDecision={onOpenQuestionDecision}
|
|
195
|
+
statusTag={questionStatusTag}
|
|
181
196
|
onOpenLinkedDoc={onOpenLinkedDoc}
|
|
182
197
|
onOpenCodeFile={onOpenCodeFile}
|
|
183
198
|
imageBaseDir={imageBaseDir}
|
|
@@ -32,6 +32,10 @@ export interface CommentAskAIContext {
|
|
|
32
32
|
/** Agent-facing identity of the selected element(s), for a pinpoint on a
|
|
33
33
|
* raw-HTML or live-app surface. Rides with the question; not displayed. */
|
|
34
34
|
detail?: string;
|
|
35
|
+
/** 1-based source lines of a markdown text selection (see
|
|
36
|
+
* `AIQuestion.scope.lineStart`). Rides with the question; not displayed. */
|
|
37
|
+
lineStart?: number;
|
|
38
|
+
lineEnd?: number;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
export type CommentAskAIHandler = (
|
|
@@ -25,7 +25,10 @@ const ChatBubbleIcon = () => (
|
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
26
26
|
|
|
27
27
|
interface CompletionOverlayProps {
|
|
28
|
-
|
|
28
|
+
/** `done`: the session finished with nothing sent (an annotate Done with
|
|
29
|
+
* no feedback). Neutral styling. `done` and `exited` (Close) send nothing,
|
|
30
|
+
* so neither shows the "response has been sent" line. */
|
|
31
|
+
submitted: 'approved' | 'denied' | 'feedback' | 'done' | 'exited' | null | false;
|
|
29
32
|
title: string;
|
|
30
33
|
subtitle: string;
|
|
31
34
|
agentLabel: string;
|
|
@@ -43,16 +46,20 @@ export function CompletionOverlay({ submitted, title, subtitle, agentLabel, link
|
|
|
43
46
|
if (!submitted) return null;
|
|
44
47
|
|
|
45
48
|
const isApproved = submitted === 'approved';
|
|
49
|
+
const isDone = submitted === 'done';
|
|
50
|
+
const sentNothing = isDone || submitted === 'exited';
|
|
46
51
|
|
|
47
52
|
return (
|
|
48
|
-
<div className="fixed inset-0 z-[100] bg-background flex items-center justify-center">
|
|
53
|
+
<div className="fixed inset-0 z-[100] bg-background flex items-center justify-center" data-completion-state={submitted}>
|
|
49
54
|
<div className="text-center space-y-6 max-w-md px-8">
|
|
50
55
|
<div
|
|
51
56
|
className={`mx-auto w-16 h-16 rounded-full flex items-center justify-center ${
|
|
52
|
-
isApproved ? 'bg-success/20 text-success'
|
|
57
|
+
isApproved ? 'bg-success/20 text-success'
|
|
58
|
+
: isDone ? 'bg-muted text-muted-foreground'
|
|
59
|
+
: 'bg-accent/20 text-accent'
|
|
53
60
|
}`}
|
|
54
61
|
>
|
|
55
|
-
{isApproved ? <CheckIcon /> : <ChatBubbleIcon />}
|
|
62
|
+
{isApproved || isDone ? <CheckIcon /> : <ChatBubbleIcon />}
|
|
56
63
|
</div>
|
|
57
64
|
|
|
58
65
|
<div className="space-y-2">
|
|
@@ -109,7 +116,7 @@ export function CompletionOverlay({ submitted, title, subtitle, agentLabel, link
|
|
|
109
116
|
</label>
|
|
110
117
|
<p className="text-xs text-muted-foreground/60">You can change the delay in Settings.</p>
|
|
111
118
|
</>
|
|
112
|
-
) : (
|
|
119
|
+
) : sentNothing ? null : (
|
|
113
120
|
<p className="text-xs text-muted-foreground/60">Your response has been sent.</p>
|
|
114
121
|
)}
|
|
115
122
|
</>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
|
-
import { type QuickLabel, getQuickLabels } from '../utils/quickLabels';
|
|
3
|
+
import { type QuickLabel, getQuickLabels, quickLabelIndexForDigit } from '../utils/quickLabels';
|
|
4
4
|
import { QuickLabelDropdown } from './QuickLabelDropdown';
|
|
5
5
|
|
|
6
6
|
interface FloatingQuickLabelPickerProps {
|
|
@@ -78,7 +78,7 @@ export const FloatingQuickLabelPicker: React.FC<FloatingQuickLabelPickerProps> =
|
|
|
78
78
|
if (isDigit && !e.ctrlKey && !e.metaKey) {
|
|
79
79
|
e.preventDefault();
|
|
80
80
|
const digit = parseInt(e.code.slice(5), 10);
|
|
81
|
-
const index = digit
|
|
81
|
+
const index = quickLabelIndexForDigit(digit);
|
|
82
82
|
if (index < quickLabels.length) {
|
|
83
83
|
onSelect(quickLabels[index]);
|
|
84
84
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { type QuickLabel, getLabelColors } from '../utils/quickLabels';
|
|
2
|
+
import { type QuickLabel, getLabelColors, quickLabelShortcutDigit } from '../utils/quickLabels';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Shared vertical label list used by both FloatingQuickLabelPicker
|
|
@@ -25,9 +25,14 @@ export const QuickLabelDropdown: React.FC<{
|
|
|
25
25
|
)}
|
|
26
26
|
{labels.map((label, index) => {
|
|
27
27
|
const colors = getLabelColors(label.color);
|
|
28
|
+
const digit = quickLabelShortcutDigit(index);
|
|
28
29
|
return (
|
|
29
30
|
<button
|
|
30
|
-
|
|
31
|
+
// Keyed by position, not label.id: ids are derived from the
|
|
32
|
+
// label text, so two labels with the same text share one (#1736).
|
|
33
|
+
// The list never reorders while open, and position is exactly
|
|
34
|
+
// what the digit key on each row names.
|
|
35
|
+
key={index}
|
|
31
36
|
onClick={() => onSelect(label)}
|
|
32
37
|
className="group w-full flex items-center gap-2 px-2 py-[5px] text-left transition-colors hover:bg-muted/60 active:bg-muted"
|
|
33
38
|
style={animate ? {
|
|
@@ -50,9 +55,9 @@ export const QuickLabelDropdown: React.FC<{
|
|
|
50
55
|
{label.text}
|
|
51
56
|
</span>
|
|
52
57
|
{/* Shortcut hint */}
|
|
53
|
-
{
|
|
58
|
+
{digit && (
|
|
54
59
|
<span className="text-[9px] tabular-nums text-muted-foreground/40 group-hover:text-muted-foreground/60 flex-shrink-0 font-mono">
|
|
55
|
-
{
|
|
60
|
+
{digit}
|
|
56
61
|
</span>
|
|
57
62
|
)}
|
|
58
63
|
</button>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useServerSessionReplaced } from '../utils/serverSession';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The stale-tab prompt (packages/ui/utils/serverSession.ts): shown once a
|
|
6
|
+
* decision was refused with `409 session_mismatch`, i.e. the Plannotator
|
|
7
|
+
* session this tab was opened for is gone and a newer one owns the address.
|
|
8
|
+
* Nothing was submitted; reloading shows what is open there now.
|
|
9
|
+
*/
|
|
10
|
+
export const ServerSessionReplacedBanner: React.FC<{ onReload?: () => void }> = ({ onReload }) => {
|
|
11
|
+
const replaced = useServerSessionReplaced();
|
|
12
|
+
if (!replaced) return null;
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
role="alert"
|
|
16
|
+
data-server-session-replaced
|
|
17
|
+
className="fixed inset-x-0 top-0 z-[110] flex items-center justify-center gap-3 border-b border-border bg-destructive/95 px-4 py-2 text-sm text-destructive-foreground shadow-md"
|
|
18
|
+
>
|
|
19
|
+
<span>This review was replaced — reload to see what is open now. Nothing was submitted.</span>
|
|
20
|
+
<button
|
|
21
|
+
type="button"
|
|
22
|
+
onClick={onReload ?? (() => window.location.reload())}
|
|
23
|
+
className="rounded border border-destructive-foreground/40 px-2 py-0.5 font-medium hover:bg-destructive-foreground/10"
|
|
24
|
+
>
|
|
25
|
+
Reload
|
|
26
|
+
</button>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
};
|