@lotics/ui 14.0.0 → 14.1.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/docs/ai_patterns.md +8 -1
- package/docs/catalog.md +7 -1
- package/package.json +2 -1
- package/src/agent_progress.tsx +6 -3
- package/src/clarify_wizard.tsx +5 -4
- package/src/follow_scroll.tsx +58 -0
package/docs/ai_patterns.md
CHANGED
|
@@ -136,6 +136,13 @@ move on, so the feed doesn't dramatize it. A BREAKING error that terminates the
|
|
|
136
136
|
OUTSIDE `parts` (chat persists it in the message's `errors`; `useAgentRun().error` carries it) —
|
|
137
137
|
pass it as the `error` prop and it renders as a terminal danger row under the transcript.
|
|
138
138
|
|
|
139
|
+
**In a bounded container, wrap it in `FollowScroll`.** An `AgentRun` streaming inside a dialog,
|
|
140
|
+
drawer, or fixed-height panel grows BELOW the fold — a plain scroll container doesn't follow. Wrap
|
|
141
|
+
the feed: `<FollowScroll style={{ maxHeight: … }}><AgentRun … /></FollowScroll>` — pinned to the
|
|
142
|
+
newest content while streaming, unpinned when the human scrolls up to read (`AgentProgress`'s
|
|
143
|
+
expanded panel already does this). A full-page inverted list (chat) follows by construction and
|
|
144
|
+
doesn't need it.
|
|
145
|
+
|
|
139
146
|
Fed natively by `@lotics/app-sdk`'s `useAgentRun().parts` (reasoning + per-tool I/O + state come
|
|
140
147
|
for free — no hand-assembly): `<AgentRun parts={run.parts} state={…} error={run.error} />` — see
|
|
141
148
|
[the SDK doc](../../app-sdk/docs/ai.md) for the hook. Requires `ai` as an optional (type-only) peer.
|
|
@@ -268,7 +275,7 @@ reads as the custom answer).
|
|
|
268
275
|
|
|
269
276
|
`ClarifyWizard` (`@lotics/ui/clarify_wizard`): a SEQUENCE of clarify questions worked one at a time —
|
|
270
277
|
Back / Next / Cancel / Submit (Next/Submit primary), the step position an eyebrow ("1 / 3") above the
|
|
271
|
-
question — no progress bar. Each step is a `Clarify`; advance only once the
|
|
278
|
+
question — no progress bar, and no eyebrow at all for a single question. Each step is a `Clarify`; advance only once the
|
|
272
279
|
current question is answered; `onSubmit` gives one `{ value, custom }` per question (aligned by index,
|
|
273
280
|
`custom` true when the answer came from "Other…"), `onCancel` dismisses. All navigation chrome is
|
|
274
281
|
locale-wired (the `clarify` slice). The multi-question form of the ask-back.
|
package/docs/catalog.md
CHANGED
|
@@ -976,6 +976,11 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
976
976
|
`AgentRun` and `AgentProgress`. The sole file permitted to `import type` from `ai`.
|
|
977
977
|
- **`agent_progress`** — `AgentProgress`: `AgentRun`'s compact, floating, expandable form (same
|
|
978
978
|
`parts` prop) — a composer's working state.
|
|
979
|
+
- **`follow_scroll`** — `FollowScroll`: a scroll container that FOLLOWS its growing content —
|
|
980
|
+
pinned to the bottom as children stream in; scrolling up unpins (read history), scrolling back
|
|
981
|
+
re-pins. Wrap a streaming `AgentRun` with it in any bounded container (a dialog, a drawer,
|
|
982
|
+
a panel — put the height bound on `style`); `AgentProgress`'s expanded panel uses it. Chat
|
|
983
|
+
doesn't need it (an inverted message list follows by construction).
|
|
979
984
|
- **`confidence`** — `Confidence` + `ConfidenceLevel` + `levelFromScore`: calibrated
|
|
980
985
|
high/med/low; localized via the provider.
|
|
981
986
|
- **`change_review`** — the COMPOUND review family — frame: `ChangeReview` provider/stack ·
|
|
@@ -995,7 +1000,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
995
1000
|
`{ value, label, description }` — the description is MANDATORY (every answer explains itself).
|
|
996
1001
|
`allowCustom` adds an always-visible borderless multiline custom-answer field below the options.
|
|
997
1002
|
- **`clarify_wizard`** — `ClarifyWizard` + `ClarifyWizardQuestion`/`ClarifyWizardAnswer`: a SEQUENCE
|
|
998
|
-
of `Clarify` questions with Back/Next/Cancel/Submit + a `n / total` position indicator
|
|
1003
|
+
of `Clarify` questions with Back/Next/Cancel/Submit + a `n / total` position indicator (omitted
|
|
1004
|
+
for a single question); advance
|
|
999
1005
|
only when the current is answered, `onSubmit` returns one `{ value, custom }` per question.
|
|
1000
1006
|
Navigation chrome is locale-wired (the `clarify` locale slice).
|
|
1001
1007
|
- **`choice_list`** — `ChoiceList` + `ChoiceOption`: selectable answer options as
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "14.
|
|
3
|
+
"version": "14.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./vite": {
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
"./activity_indicator": "./src/activity_indicator.tsx",
|
|
81
81
|
"./agent_run": "./src/agent_run.tsx",
|
|
82
82
|
"./agent_progress": "./src/agent_progress.tsx",
|
|
83
|
+
"./follow_scroll": "./src/follow_scroll.tsx",
|
|
83
84
|
"./markdown": {
|
|
84
85
|
"react-native": "./src/markdown.tsx",
|
|
85
86
|
"default": "./src/markdown.web.tsx"
|
package/src/agent_progress.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { StyleSheet, View } from "react-native";
|
|
3
3
|
import { colors } from "./colors";
|
|
4
4
|
import { Text } from "./text";
|
|
5
5
|
import { WaveAvatar } from "./wave_avatar";
|
|
6
|
+
import { FollowScroll } from "./follow_scroll";
|
|
6
7
|
import { PressableHighlight } from "./pressable_highlight";
|
|
7
8
|
import { AgentRun, resolveToolMeta } from "./agent_run";
|
|
8
9
|
import { toSegments, lastRunningStep, type AgentUIPart } from "./agent_transform";
|
|
@@ -46,9 +47,11 @@ export function AgentProgress(props: AgentProgressProps) {
|
|
|
46
47
|
<View style={{ gap: 8 }}>
|
|
47
48
|
{expanded ? (
|
|
48
49
|
<View style={styles.panel}>
|
|
49
|
-
|
|
50
|
+
{/* FollowScroll keeps the newest step in view as the run streams —
|
|
51
|
+
a plain ScrollView would let new content grow below the fold. */}
|
|
52
|
+
<FollowScroll style={{ maxHeight: 260 }} contentContainerStyle={{ padding: 16 }}>
|
|
50
53
|
<AgentRun parts={parts} state={state} error={error} labelForTool={labelForTool} stepsLabel={stepsLabel} />
|
|
51
|
-
</
|
|
54
|
+
</FollowScroll>
|
|
52
55
|
</View>
|
|
53
56
|
) : null}
|
|
54
57
|
|
package/src/clarify_wizard.tsx
CHANGED
|
@@ -64,10 +64,11 @@ export function ClarifyWizard(props: ClarifyWizardProps) {
|
|
|
64
64
|
|
|
65
65
|
return (
|
|
66
66
|
<View style={{ gap: 16 }}>
|
|
67
|
-
{/* The step position is an eyebrow above the question (no progress bar
|
|
68
|
-
footer aligns to the same 8px inset
|
|
69
|
-
index so each step's custom-answer
|
|
70
|
-
|
|
67
|
+
{/* The step position is an eyebrow above the question (no progress bar — and
|
|
68
|
+
none at all for a single question); the footer aligns to the same 8px inset
|
|
69
|
+
as the question and answers. Keyed by index so each step's custom-answer
|
|
70
|
+
draft is its own (no bleed across steps). */}
|
|
71
|
+
<Clarify key={index} eyebrow={total > 1 ? `${index + 1} / ${total}` : undefined} question={q.question} options={q.answers} answer={current} onAnswer={setAnswer} allowCustom={q.allowCustom} />
|
|
71
72
|
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 8, paddingHorizontal: 8 }}>
|
|
72
73
|
<Button title={labels.cancel} color="muted" onPress={onCancel} />
|
|
73
74
|
<View style={{ flexDirection: "row", gap: 8 }}>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useRef, type ReactNode } from "react";
|
|
2
|
+
import { ScrollView, StyleProp, ViewStyle, type NativeScrollEvent, type NativeSyntheticEvent } from "react-native";
|
|
3
|
+
|
|
4
|
+
export interface FollowScrollProps {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
/** The frame — put the height bound here (`maxHeight` / `height` / `flex`);
|
|
7
|
+
* without one the container never overflows and there is nothing to follow. */
|
|
8
|
+
style?: StyleProp<ViewStyle>;
|
|
9
|
+
contentContainerStyle?: StyleProp<ViewStyle>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// "At the bottom" tolerance: within this many px of the end still counts as
|
|
13
|
+
// pinned, so sub-pixel scroll positions and streaming-growth rounding never
|
|
14
|
+
// silently unpin the follow.
|
|
15
|
+
const PIN_THRESHOLD = 40;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A scroll container that FOLLOWS its growing content — pinned to the bottom, it
|
|
19
|
+
* keeps the newest content in view as children stream in (an `AgentRun` in a
|
|
20
|
+
* dialog, a log feed). Scrolling up UNPINS it so the human can read history while
|
|
21
|
+
* the content keeps growing; scrolling back to the bottom re-pins. Chat doesn't
|
|
22
|
+
* need this (its message list is inverted, which follows by construction) — this
|
|
23
|
+
* is for the plain top-down containers everywhere else: dialogs, drawers, panels.
|
|
24
|
+
*/
|
|
25
|
+
export function FollowScroll(props: FollowScrollProps) {
|
|
26
|
+
const { children, style, contentContainerStyle } = props;
|
|
27
|
+
const scrollRef = useRef<ScrollView>(null);
|
|
28
|
+
// Pinned state lives in a ref — it changes on every scroll tick and must never
|
|
29
|
+
// re-render the (streaming, already-busy) subtree.
|
|
30
|
+
const pinnedRef = useRef(true);
|
|
31
|
+
|
|
32
|
+
const followIfPinned = () => {
|
|
33
|
+
if (pinnedRef.current) scrollRef.current?.scrollToEnd({ animated: false });
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
37
|
+
const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;
|
|
38
|
+
const distanceFromBottom = contentSize.height - contentOffset.y - layoutMeasurement.height;
|
|
39
|
+
pinnedRef.current = distanceFromBottom < PIN_THRESHOLD;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<ScrollView
|
|
44
|
+
ref={scrollRef}
|
|
45
|
+
style={style}
|
|
46
|
+
contentContainerStyle={contentContainerStyle}
|
|
47
|
+
// Content grew (a new part streamed in) or the frame resized (dialog
|
|
48
|
+
// re-measured) — both move the bottom edge, so both re-follow when pinned.
|
|
49
|
+
onContentSizeChange={followIfPinned}
|
|
50
|
+
onLayout={followIfPinned}
|
|
51
|
+
onScroll={onScroll}
|
|
52
|
+
scrollEventThrottle={16}
|
|
53
|
+
nestedScrollEnabled
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
</ScrollView>
|
|
57
|
+
);
|
|
58
|
+
}
|