@pinagent/react-native 0.2.4 → 0.2.5
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/dist/native/AgentDock.d.ts +32 -0
- package/dist/native/MarkdownView.d.ts +24 -0
- package/dist/native/Pinagent.d.ts +5 -4
- package/dist/native/StreamSheet.d.ts +18 -13
- package/dist/native/inspector.d.ts +24 -9
- package/dist/native/keyboard-height.d.ts +2 -0
- package/dist/native/keyboard.d.ts +35 -0
- package/dist/native/markdown.d.ts +65 -0
- package/dist/native/run-state.d.ts +120 -0
- package/dist/native/scroll-follow.d.ts +32 -0
- package/dist/native/ws-client.d.ts +8 -2
- package/package.json +1 -1
- package/src/native/AgentDock.tsx +232 -0
- package/src/native/MarkdownView.tsx +154 -0
- package/src/native/Pinagent.tsx +80 -58
- package/src/native/StreamSheet.tsx +188 -135
- package/src/native/inspector.ts +70 -31
- package/src/native/keyboard-height.ts +34 -0
- package/src/native/keyboard.ts +40 -0
- package/src/native/markdown.ts +194 -0
- package/src/native/run-state.ts +204 -0
- package/src/native/scroll-follow.ts +47 -0
- package/src/native/ws-client.ts +13 -5
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact dock for minimized agent runs (bottom-left).
|
|
3
|
+
*
|
|
4
|
+
* Replaces the old one-fat-pill-per-run stack. It renders the pure
|
|
5
|
+
* {@link dockModel} aggregation:
|
|
6
|
+
*
|
|
7
|
+
* - **Active runs** (connecting/working/awaiting) follow a hybrid rule — a
|
|
8
|
+
* single slim chip when one runs, and a collapsed `◐ N agents · M needs you`
|
|
9
|
+
* count bar (tap to expand the chip list) once two or more do.
|
|
10
|
+
* - **Finished runs** (done/failed) always roll into a `▸ N finished` summary
|
|
11
|
+
* you tap to review and clear.
|
|
12
|
+
*
|
|
13
|
+
* Each `StreamSheet` stays mounted (it renders `null` while minimized) so its
|
|
14
|
+
* WebSocket keeps streaming; this dock is fed each run's derived `state` from
|
|
15
|
+
* the parent and is purely presentational. Tapping a chip expands that run's
|
|
16
|
+
* full sheet; ✕ tears it down.
|
|
17
|
+
*
|
|
18
|
+
* All decision logic (partitioning, ordering, headline text) lives in the pure
|
|
19
|
+
* `run-state` module — this file is intentionally just layout + animation, the
|
|
20
|
+
* un-testable RN-runtime layer.
|
|
21
|
+
*/
|
|
22
|
+
import type { ReactElement } from 'react';
|
|
23
|
+
import { type DockRun } from './run-state';
|
|
24
|
+
export interface AgentDockProps {
|
|
25
|
+
/** Minimized runs with their derived state (the expanded one is excluded). */
|
|
26
|
+
runs: DockRun[];
|
|
27
|
+
/** Expand a run to its full sheet. */
|
|
28
|
+
onExpand: (id: string) => void;
|
|
29
|
+
/** Tear a run down (stops its WS, removes it). */
|
|
30
|
+
onClose: (id: string) => void;
|
|
31
|
+
}
|
|
32
|
+
export declare function AgentDock({ runs, onExpand, onClose }: AgentDockProps): ReactElement | null;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render agent Markdown with React Native primitives.
|
|
3
|
+
*
|
|
4
|
+
* `parseMarkdown` (markdown.ts) does the parsing; this maps its block/inline
|
|
5
|
+
* tree onto <View>/<Text>. Inline marks ride as nested <Text> inside the
|
|
6
|
+
* paragraph's <Text>, which inherits the caller's `baseStyle` (the sheet's text
|
|
7
|
+
* row), so plain prose keeps its existing look and only the marked-up runs pick
|
|
8
|
+
* up bold/italic/code/link styling. Like the rest of `src/native`, it leans on
|
|
9
|
+
* RN core only — no markdown renderer dependency — so it ships as source and
|
|
10
|
+
* Metro bundles it onto the device unchanged.
|
|
11
|
+
*
|
|
12
|
+
* The file is `MarkdownView` (not `Markdown`) so it doesn't collide with
|
|
13
|
+
* `markdown.ts` on case-insensitive filesystems — tsc treats the two as the
|
|
14
|
+
* same module otherwise.
|
|
15
|
+
*/
|
|
16
|
+
import type { ReactElement } from 'react';
|
|
17
|
+
import type { StyleProp } from 'react-native';
|
|
18
|
+
import { type TextStyle } from 'react-native';
|
|
19
|
+
export interface MarkdownViewProps {
|
|
20
|
+
text: string;
|
|
21
|
+
/** Base text style for paragraphs/inline runs (the sheet's text row). */
|
|
22
|
+
baseStyle?: StyleProp<TextStyle>;
|
|
23
|
+
}
|
|
24
|
+
export declare function MarkdownView({ text, baseStyle }: MarkdownViewProps): ReactElement;
|
|
@@ -24,10 +24,11 @@
|
|
|
24
24
|
* (MCP) pickup. "+ Add element" multi-picks several targets into one comment
|
|
25
25
|
* (sent as `additionalAnchors`); a single pick leaves them null.
|
|
26
26
|
*
|
|
27
|
-
* The transcript sheet can be minimized
|
|
28
|
-
* another element and spawn a second agent. Each run
|
|
29
|
-
* so multiple agents can stream concurrently — the
|
|
30
|
-
* sheet; the rest
|
|
27
|
+
* The transcript sheet can be minimized into the compact bottom-left dock,
|
|
28
|
+
* freeing the screen to pick another element and spawn a second agent. Each run
|
|
29
|
+
* keeps its own live sheet, so multiple agents can stream concurrently — the
|
|
30
|
+
* expanded one shows its full sheet; the rest collapse into the dock (a chip, or
|
|
31
|
+
* a count bar at 2+) and keep streaming until tapped open.
|
|
31
32
|
*/
|
|
32
33
|
import type { ReactElement } from 'react';
|
|
33
34
|
export interface PinagentProps {
|
|
@@ -13,27 +13,32 @@
|
|
|
13
13
|
* reconnect replays the agent transcript, so we clear `events` on `onReset`
|
|
14
14
|
* but keep local follow-ups.
|
|
15
15
|
*
|
|
16
|
-
* The sheet can be **minimized
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
16
|
+
* The sheet can be **minimized**: the run drops into the compact bottom-left
|
|
17
|
+
* `AgentDock` (a chip / count bar) so the developer can keep interacting with
|
|
18
|
+
* the app — e.g. to pick another element and spawn a second agent. Minimizing
|
|
19
|
+
* doesn't tear the run down: this component stays mounted and simply renders
|
|
20
|
+
* `null` (the dock draws the compact UI), so the WebSocket keeps streaming in
|
|
21
|
+
* the background and the live transcript is intact the moment it's re-expanded.
|
|
22
|
+
* `<Pinagent/>` mounts one of these per concurrent run and reads each run's
|
|
23
|
+
* derived {@link RunState} (reported via `onState`) to drive the dock.
|
|
22
24
|
*/
|
|
23
25
|
import type { ReactElement } from 'react';
|
|
26
|
+
import { type RunState } from './run-state';
|
|
24
27
|
export interface StreamSheetProps {
|
|
25
28
|
feedbackId: string;
|
|
26
29
|
/** Source label shown in the header (e.g. `file:line` or component name). */
|
|
27
30
|
target: string;
|
|
28
|
-
/**
|
|
31
|
+
/** Minimized → render nothing (the dock shows the compact chip); WS stays live. */
|
|
29
32
|
minimized: boolean;
|
|
30
|
-
/**
|
|
31
|
-
stackIndex: number;
|
|
32
|
-
/** Collapse the full sheet to its pill. */
|
|
33
|
+
/** Collapse the full sheet back into the dock. */
|
|
33
34
|
onMinimize: () => void;
|
|
34
|
-
/** Expand the pill back to the full sheet. */
|
|
35
|
-
onExpand: () => void;
|
|
36
35
|
/** Dismiss for good — tears down the WS and removes this run's view. */
|
|
37
36
|
onClose: () => void;
|
|
37
|
+
/**
|
|
38
|
+
* Report the run's derived state up so the dock can render it. `interrupting`
|
|
39
|
+
* is the Stop overlay (ticket 015) — the developer tapped Stop and we're
|
|
40
|
+
* awaiting teardown; the dock relabels the chip to "Stopping…" while active.
|
|
41
|
+
*/
|
|
42
|
+
onState: (state: RunState, interrupting: boolean) => void;
|
|
38
43
|
}
|
|
39
|
-
export declare function StreamSheet({ feedbackId, target, minimized,
|
|
44
|
+
export declare function StreamSheet({ feedbackId, target, minimized, onMinimize, onClose, onState, }: StreamSheetProps): ReactElement | null;
|
|
@@ -156,16 +156,31 @@ interface MeasuredHit {
|
|
|
156
156
|
* `react-native-pager-view` (and anything that hosts content in a native
|
|
157
157
|
* container) detaches a page's *native* views from the Fabric shadow tree
|
|
158
158
|
* `findNodeAtPoint` walks, so the native hit-test bottoms out at the page
|
|
159
|
-
* wrapper
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
159
|
+
* wrapper — it returns no touched instance at all (`closestPublicInstance` is
|
|
160
|
+
* null). But the React **fiber** tree is intact and the page's widgets are
|
|
161
|
+
* on-screen, hence measurable — so we hit-test ourselves: DFS the fiber subtree
|
|
162
|
+
* under `root` (the app root, an ancestor of every on-screen view), measuring
|
|
163
|
+
* each host, and keep the DEEPEST tagged host inside the tapped region.
|
|
163
164
|
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
165
|
+
* `region` threads the frame of the nearest measurable host ancestor that
|
|
166
|
+
* CONTAINS the tap (null until we enter one). Two subtleties this handles:
|
|
167
|
+
*
|
|
168
|
+
* - **Pruning.** A host with a real frame that MISSES the tap can't have a
|
|
169
|
+
* (non-overflowing) descendant that hits, so its subtree is skipped — like
|
|
170
|
+
* the browser's `elementFromPoint`.
|
|
171
|
+
* - **Flattened / detached hosts.** RN flattens layout-only `<View>`s (no
|
|
172
|
+
* native view → `measure` returns null) and pager pages are detached, so a
|
|
173
|
+
* widget's own tagged hosts often can't be measured. Once we're inside a
|
|
174
|
+
* measurable containing region we keep recording tagged hosts even when they
|
|
175
|
+
* can't be measured (they borrow the region's frame for the highlight) and
|
|
176
|
+
* descend through them — otherwise geometry bottoms out at the outermost
|
|
177
|
+
* non-flattened wrapper (e.g. an animated card) and every widget collapses to
|
|
178
|
+
* that shared wrapper's source. Measurable siblings still prune wrong
|
|
179
|
+
* branches, so we stay within the tapped element.
|
|
180
|
+
*
|
|
181
|
+
* Composite / fragment fibers carry no frame, so we always descend through them
|
|
182
|
+
* to reach their hosts. `measure` is injected so the traversal is unit-testable
|
|
183
|
+
* without an RN runtime.
|
|
169
184
|
*/
|
|
170
185
|
export declare function measureHitTest(root: FiberLike | null, x: number, y: number, measure: (fiber: FiberLike) => Promise<Frame | null>): Promise<MeasuredHit | null>;
|
|
171
186
|
/**
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard-shortcut helpers for the React Native widget.
|
|
3
|
+
*
|
|
4
|
+
* The browser widget wires its hotkeys to `document`-level `keydown`
|
|
5
|
+
* listeners (see packages/widget/src/keyboard.ts). React Native has no such
|
|
6
|
+
* global key stream in JS: the core `Keyboard` module only reports the soft
|
|
7
|
+
* keyboard showing/hiding, never key presses, and there's no document to
|
|
8
|
+
* listen on. A faithful port of the *global* web hotkeys (toggle picker,
|
|
9
|
+
* hop-to-next-agent, minimize-all) would need a native module — iOS
|
|
10
|
+
* `UIKeyCommand` / an Android key bridge — which we deliberately avoid: the RN
|
|
11
|
+
* widget ships as plain JS source for Metro with zero native setup.
|
|
12
|
+
*
|
|
13
|
+
* What IS reachable, and all we need in practice, are the key events a focused
|
|
14
|
+
* `TextInput` surfaces — so the shortcuts live where a hardware keyboard is
|
|
15
|
+
* actually in play (the composer and the stream sheet, both modal):
|
|
16
|
+
* - Return/Enter, via `onSubmitEditing` on the single-line inputs — submits
|
|
17
|
+
* the agent answer and the follow-up, mirroring the web composer's
|
|
18
|
+
* Enter-to-send. Wired directly on the input (no key inspection needed).
|
|
19
|
+
* - Escape, via `onKeyPress` — backs out of a sheet, mirroring the web
|
|
20
|
+
* widget's Escape. Decided here so the (RN-runtime-only, untestable)
|
|
21
|
+
* components stay thin and the rule is unit-tested.
|
|
22
|
+
*
|
|
23
|
+
* `onKeyPress`'s `nativeEvent` only carries `key` on native platforms (no
|
|
24
|
+
* modifier flags), so these predicates take the bare key name — there's no
|
|
25
|
+
* Shift/Cmd to branch on, which is also why plain Enter stays a newline in the
|
|
26
|
+
* multiline composer rather than hijacking submit. Hardware Back (Android) is
|
|
27
|
+
* handled separately by each Modal's `onRequestClose`.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Should this key event back out of the current sheet? Escape on a hardware
|
|
31
|
+
* keyboard — the RN analog of the web widget's Escape. Only the Escape key
|
|
32
|
+
* qualifies; every printable key (i.e. normal typing) is ignored, so an
|
|
33
|
+
* `onKeyPress` handler built on this never interferes with text entry.
|
|
34
|
+
*/
|
|
35
|
+
export declare function isDismissKey(key: string): boolean;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny, dependency-free Markdown parser for the RN widget.
|
|
3
|
+
*
|
|
4
|
+
* The agent streams its replies as Markdown — Claude writes **bold**, `code`,
|
|
5
|
+
* fenced blocks, bullet/numbered lists, headings and links. The StreamSheet
|
|
6
|
+
* used to drop that straight into a <Text>, so the markers showed up as
|
|
7
|
+
* literal characters. This module folds the raw text into a small block/inline
|
|
8
|
+
* tree that `Markdown.tsx` renders with React Native primitives — no
|
|
9
|
+
* markdown-it, no extra runtime dependency, in keeping with the rest of the
|
|
10
|
+
* native source (see transcript.ts for the same "mirror, don't import" stance).
|
|
11
|
+
*
|
|
12
|
+
* It is intentionally a *subset* of CommonMark covering what shows up in chat:
|
|
13
|
+
* ATX headings, fenced code, blockquotes, unordered/ordered lists, thematic
|
|
14
|
+
* breaks, paragraphs, and inline code / bold / italic / links. Anything it
|
|
15
|
+
* doesn't recognise falls through as plain text, so the worst case degrades to
|
|
16
|
+
* the old behaviour rather than dropping content. Pure and deterministic, so
|
|
17
|
+
* it's unit-tested like the transcript reducer.
|
|
18
|
+
*/
|
|
19
|
+
/** An inline run carrying at most the marks we render. */
|
|
20
|
+
export interface InlineSpan {
|
|
21
|
+
text: string;
|
|
22
|
+
bold?: boolean;
|
|
23
|
+
italic?: boolean;
|
|
24
|
+
code?: boolean;
|
|
25
|
+
/** Present on link spans; the destination URL. */
|
|
26
|
+
href?: string;
|
|
27
|
+
}
|
|
28
|
+
export type MdBlock = {
|
|
29
|
+
type: 'heading';
|
|
30
|
+
level: number;
|
|
31
|
+
spans: InlineSpan[];
|
|
32
|
+
} | {
|
|
33
|
+
type: 'paragraph';
|
|
34
|
+
spans: InlineSpan[];
|
|
35
|
+
} | {
|
|
36
|
+
type: 'code';
|
|
37
|
+
text: string;
|
|
38
|
+
lang?: string;
|
|
39
|
+
} | {
|
|
40
|
+
type: 'list';
|
|
41
|
+
ordered: boolean;
|
|
42
|
+
items: InlineSpan[][];
|
|
43
|
+
} | {
|
|
44
|
+
type: 'quote';
|
|
45
|
+
spans: InlineSpan[];
|
|
46
|
+
} | {
|
|
47
|
+
type: 'hr';
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Inline scanner: walk the string and, at each step, take the earliest of the
|
|
51
|
+
* recognised markers — ties broken by priority (code > link > bold > italic),
|
|
52
|
+
* so `**x**` reads as one bold run rather than two italics, and a backtick span
|
|
53
|
+
* is never re-parsed for emphasis. Text between markers is emitted verbatim;
|
|
54
|
+
* an unbalanced marker (a lone `*`) never matches and stays literal, so we
|
|
55
|
+
* never swallow content.
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseInline(input: string): InlineSpan[];
|
|
58
|
+
/**
|
|
59
|
+
* Fold raw Markdown into render-ready blocks. Line-oriented and greedy:
|
|
60
|
+
* consecutive list items fold into one list, consecutive `>` lines into one
|
|
61
|
+
* quote, and consecutive plain lines into one paragraph (soft-wrapped lines are
|
|
62
|
+
* joined with a space). Blank lines separate paragraphs. Pure and
|
|
63
|
+
* deterministic.
|
|
64
|
+
*/
|
|
65
|
+
export declare function parseMarkdown(source: string): MdBlock[];
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure run-state model for the RN widget's live agent runs.
|
|
3
|
+
*
|
|
4
|
+
* The minimized agent UI used to derive its status ad-hoc inside `StreamSheet`
|
|
5
|
+
* from a tangle of booleans (`running`/`done`/`transportError`/`askOpen`),
|
|
6
|
+
* encoded purely by dot color. This module replaces that with one explicit,
|
|
7
|
+
* unit-testable state machine: a {@link RunState} per run, a presentation map
|
|
8
|
+
* (glyph + tone + label, not color-alone), and the aggregation
|
|
9
|
+
* ({@link dockModel}) the compact bottom-left dock renders.
|
|
10
|
+
*
|
|
11
|
+
* It's deliberately dependency-free (only the sibling `transcript` reducer) so
|
|
12
|
+
* it's testable without React Native — the device UI (`AgentDock`, the expanded
|
|
13
|
+
* `StreamSheet`) stays a thin renderer over these pure functions, the same way
|
|
14
|
+
* `transcript.ts` / `restore.ts` keep their logic out of the un-testable
|
|
15
|
+
* RN-runtime layer.
|
|
16
|
+
*/
|
|
17
|
+
import { type AgentEvent } from './transcript';
|
|
18
|
+
/**
|
|
19
|
+
* The lifecycle state of one streamed agent run.
|
|
20
|
+
*
|
|
21
|
+
* - `connecting` — socket up, transcript not replayed yet (no events). The
|
|
22
|
+
* brief window after spawn/restore and again right after a reconnect.
|
|
23
|
+
* - `working` — events are flowing and the run hasn't ended.
|
|
24
|
+
* - `awaiting` — blocked on an `ask_user` the developer hasn't answered. The
|
|
25
|
+
* one state that needs the developer's attention; it pulses.
|
|
26
|
+
* - `done` — ended cleanly (success result, or the bus simply closed).
|
|
27
|
+
* - `failed` — ended on an error (server error frame, transport error, or a
|
|
28
|
+
* non-success result subtype).
|
|
29
|
+
*/
|
|
30
|
+
export type RunState = 'connecting' | 'working' | 'awaiting' | 'done' | 'failed';
|
|
31
|
+
export interface RunStateInput {
|
|
32
|
+
/** The folded agent event stream (same array `StreamSheet` accumulates). */
|
|
33
|
+
events: AgentEvent[];
|
|
34
|
+
/** Set once the run reached a terminal `result`/`done`. */
|
|
35
|
+
done: boolean;
|
|
36
|
+
/** Non-null when a server `error` frame (or "no dev server") arrived. */
|
|
37
|
+
transportError: string | null;
|
|
38
|
+
/** `askId` → answer, so an answered question no longer reads as `awaiting`. */
|
|
39
|
+
answered: Record<string, string>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Collapse the raw run booleans into a single {@link RunState}. Pure and
|
|
43
|
+
* order-sensitive: a transport error wins over everything (you can't answer or
|
|
44
|
+
* keep working over a dead socket), then an unanswered question, then terminal
|
|
45
|
+
* done/failure, else working-vs-connecting by whether any event has arrived.
|
|
46
|
+
*/
|
|
47
|
+
export declare function deriveRunState({ events, done, transportError, answered, }: RunStateInput): RunState;
|
|
48
|
+
/** Semantic color bucket; mapped to concrete colors in the component layer. */
|
|
49
|
+
export type RunTone = 'neutral' | 'active' | 'attention' | 'success' | 'danger';
|
|
50
|
+
export interface RunPresentation {
|
|
51
|
+
/** Short status word for labels / accessibility. */
|
|
52
|
+
label: string;
|
|
53
|
+
/** Monochrome glyph so state never rides on color alone. */
|
|
54
|
+
glyph: string;
|
|
55
|
+
/** Semantic tone the renderer maps to a palette entry. */
|
|
56
|
+
tone: RunTone;
|
|
57
|
+
/** Whether the chip should pulse to pull the developer back. */
|
|
58
|
+
pulse: boolean;
|
|
59
|
+
/** Non-terminal (connecting/working/awaiting) — i.e. still an active run. */
|
|
60
|
+
active: boolean;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Does an "interrupting" overlay actually apply right now?
|
|
64
|
+
*
|
|
65
|
+
* Stop is purely a client-side affordance over the existing `interrupt` frame
|
|
66
|
+
* (ticket 015): tapping it sets a local flag, and we keep showing "Stopping…"
|
|
67
|
+
* until the server lands a terminal event. The overlay therefore applies only
|
|
68
|
+
* while the run is still {@link RunPresentation.active active} — once it reaches
|
|
69
|
+
* a terminal `done`/`failed` state the interrupt resolved, so the overlay drops
|
|
70
|
+
* even if the caller's flag hasn't been cleared yet. Modeling it as an overlay
|
|
71
|
+
* (rather than a sixth {@link RunState}) keeps the state machine — and the dock
|
|
72
|
+
* aggregation — unchanged and unit-testable.
|
|
73
|
+
*/
|
|
74
|
+
export declare function interruptOverlayActive(state: RunState, interrupting: boolean): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Presentation for a run, optionally overlaid with an interrupting affordance.
|
|
77
|
+
*
|
|
78
|
+
* With `interrupting` true on a still-active run we relabel to "Stopping…" and
|
|
79
|
+
* stop any pulse (the developer asked it to halt — pulsing for attention is the
|
|
80
|
+
* wrong signal), keeping the underlying state's glyph/tone/active so the dock
|
|
81
|
+
* partitioning is undisturbed. Terminal states ignore the flag (see
|
|
82
|
+
* {@link interruptOverlayActive}).
|
|
83
|
+
*/
|
|
84
|
+
export declare function runPresentation(state: RunState, interrupting?: boolean): RunPresentation;
|
|
85
|
+
/** A run as the dock sees it: identity, header label, and derived state. */
|
|
86
|
+
export interface DockRun {
|
|
87
|
+
id: string;
|
|
88
|
+
/** Header label — `file:line` if anchored, else the component name. */
|
|
89
|
+
target: string;
|
|
90
|
+
state: RunState;
|
|
91
|
+
/**
|
|
92
|
+
* The developer tapped Stop and we're awaiting the run's terminal event
|
|
93
|
+
* (ticket 015). An overlay over `state`, not a state — the chip relabels to
|
|
94
|
+
* "Stopping…" while it's still active. Ignored once terminal.
|
|
95
|
+
*/
|
|
96
|
+
interrupting?: boolean;
|
|
97
|
+
}
|
|
98
|
+
export interface DockModel {
|
|
99
|
+
/** connecting/working/awaiting, sorted attention-first (stable within tone). */
|
|
100
|
+
active: DockRun[];
|
|
101
|
+
/** done/failed, in input order. */
|
|
102
|
+
finished: DockRun[];
|
|
103
|
+
/** Collapse the active runs into a single count bar (true at ≥2 active). */
|
|
104
|
+
collapseActive: boolean;
|
|
105
|
+
/** How many active runs are blocked on input. */
|
|
106
|
+
awaitingCount: number;
|
|
107
|
+
/** State driving the collapsed bar's glyph/tone (most attention-worthy). */
|
|
108
|
+
summaryState: RunState;
|
|
109
|
+
/** Bar text, e.g. `3 agents · 1 needs you`. */
|
|
110
|
+
activeHeadline: string;
|
|
111
|
+
/** True when any finished run failed (lets the summary flag failures). */
|
|
112
|
+
finishedHasFailure: boolean;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Aggregate runs into what the dock draws. Active runs surface attention-first
|
|
116
|
+
* (a blocked run jumps above a busy one); ≥2 of them collapse into one count
|
|
117
|
+
* bar. Finished runs always roll into a `▸ N finished` summary. Pure so the
|
|
118
|
+
* hybrid + done-summary behavior is covered without rendering anything.
|
|
119
|
+
*/
|
|
120
|
+
export declare function dockModel(runs: readonly DockRun[]): DockModel;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure scroll-follow predicate for the RN widget's live transcript.
|
|
3
|
+
*
|
|
4
|
+
* The expanded `StreamSheet` transcript used to call `scrollToEnd` on every
|
|
5
|
+
* content change, so scrolling up to re-read earlier output during an active run
|
|
6
|
+
* got yanked back to the bottom by the next event. The fix is the standard
|
|
7
|
+
* chat-log "stick to bottom only while already near the bottom" behavior — and
|
|
8
|
+
* the near-bottom test is the one piece of math worth pulling out of the
|
|
9
|
+
* un-testable RN-runtime layer (mirrors how `run-state.ts` / `transcript.ts`
|
|
10
|
+
* keep their logic pure, see packages/react-native/src/native).
|
|
11
|
+
*/
|
|
12
|
+
/** Default slack (in px) for treating "almost at the bottom" as "at bottom". */
|
|
13
|
+
export declare const NEAR_BOTTOM_THRESHOLD = 24;
|
|
14
|
+
export interface NearBottomInput {
|
|
15
|
+
/** `contentOffset.y` — how far the content is scrolled up from the top. */
|
|
16
|
+
offsetY: number;
|
|
17
|
+
/** `layoutMeasurement.height` — the visible viewport height. */
|
|
18
|
+
viewportH: number;
|
|
19
|
+
/** `contentSize.height` — the full scrollable content height. */
|
|
20
|
+
contentH: number;
|
|
21
|
+
/** Slack in px; the gap to the bottom that still counts as "at bottom". */
|
|
22
|
+
threshold?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* True when the scroll position is within `threshold` px of the bottom (so
|
|
26
|
+
* auto-follow should keep pinning new content into view). Pure and tolerant of
|
|
27
|
+
* the bouncy/over-scroll values RN can report: a negative remaining distance
|
|
28
|
+
* (rubber-band past the end) still reads as at-bottom, and content shorter than
|
|
29
|
+
* the viewport is always at-bottom. A non-finite or non-positive threshold
|
|
30
|
+
* falls back to {@link NEAR_BOTTOM_THRESHOLD}.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isNearBottom({ offsetY, viewportH, contentH, threshold, }: NearBottomInput): boolean;
|
|
@@ -45,10 +45,16 @@ export declare class StreamClient {
|
|
|
45
45
|
sendUserMessage(content: string): void;
|
|
46
46
|
/** Answer an `ask_user` prompt. */
|
|
47
47
|
sendAskResponse(askId: string, answer: string): void;
|
|
48
|
-
/**
|
|
49
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Interrupt the in-flight run. Returns whether the frame was actually written
|
|
50
|
+
* to an OPEN socket — `false` means it was dropped (socket closed/reconnecting)
|
|
51
|
+
* so the UI can say "couldn't stop" instead of silently no-opping. The server
|
|
52
|
+
* tears the run down on its own terminal event; this is just the request.
|
|
53
|
+
*/
|
|
54
|
+
interrupt(): boolean;
|
|
50
55
|
private connect;
|
|
51
56
|
private onMessage;
|
|
57
|
+
/** Write a frame if the socket is OPEN. Returns whether it was sent. */
|
|
52
58
|
private send;
|
|
53
59
|
private scheduleReconnect;
|
|
54
60
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pinagent/react-native",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "React Native & Expo plugin for Pinagent — tap a view, leave a comment, and a coding agent fixes it with file:line + a screenshot. The native widget ships as source for Metro; the dev-server middleware and Babel source-tagging plugin are built for Node.",
|
|
6
6
|
"keywords": [
|