@pinagent/react-native 0.2.3 → 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.
@@ -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 to a pill, freeing the screen to pick
28
- * another element and spawn a second agent. Each run keeps its own live sheet,
29
- * so multiple agents can stream concurrently — the expanded one shows its full
30
- * sheet; the rest sit as pills that keep streaming until tapped open.
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** to a compact status pill so the developer can
17
- * keep interacting with the app e.g. to pick another element and spawn a
18
- * second agent. Minimizing doesn't tear the run down: the component stays
19
- * mounted (only its rendering changes), so the WebSocket keeps streaming in the
20
- * background and the live transcript is intact the moment it's re-expanded.
21
- * `<Pinagent/>` mounts one of these per concurrent run.
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
- /** Render as a compact pill (WS stays live) instead of the full sheet. */
31
+ /** Minimized render nothing (the dock shows the compact chip); WS stays live. */
29
32
  minimized: boolean;
30
- /** Stack position among minimized pills (0 = bottom-most), for layout. */
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, stackIndex, onMinimize, onExpand, onClose, }: StreamSheetProps): ReactElement;
44
+ export declare function StreamSheet({ feedbackId, target, minimized, onMinimize, onClose, onState, }: StreamSheetProps): ReactElement | null;
@@ -76,6 +76,9 @@ interface RawInspectorData {
76
76
  interface FiberLike {
77
77
  tag?: number;
78
78
  return?: FiberLike | null;
79
+ /** First child / next sibling — the render-tree walk for the measure fallback. */
80
+ child?: FiberLike | null;
81
+ sibling?: FiberLike | null;
79
82
  stateNode?: {
80
83
  canonical?: {
81
84
  publicInstance?: unknown;
@@ -137,6 +140,63 @@ export declare function crumbsOf(data: RawInspectorData): RawCrumb[];
137
140
  * stuck measure can't hang the pick).
138
141
  */
139
142
  export declare function measureFrame(measure?: (cb: RawMeasureCb) => void): Promise<Frame | null>;
143
+ /** True when window-coordinate point (x, y) lies within frame `f`. */
144
+ export declare function frameContains(f: Frame, x: number, y: number): boolean;
145
+ /** A measure-resolved hit: the deepest tagged host whose frame holds the tap. */
146
+ interface MeasuredHit {
147
+ fiber: FiberLike;
148
+ loc: Loc;
149
+ name: string | null;
150
+ frame: Frame;
151
+ }
152
+ /**
153
+ * Measure-based hit-test — the fallback for when RN's geometric
154
+ * `findNodeAtPoint` can't descend to the tapped element.
155
+ *
156
+ * `react-native-pager-view` (and anything that hosts content in a native
157
+ * container) detaches a page's *native* views from the Fabric shadow tree
158
+ * `findNodeAtPoint` walks, so the native hit-test bottoms out at the page
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.
164
+ *
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.
184
+ */
185
+ export declare function measureHitTest(root: FiberLike | null, x: number, y: number, measure: (fiber: FiberLike) => Promise<Frame | null>): Promise<MeasuredHit | null>;
186
+ /**
187
+ * The chain of distinctly-located tagged hosts from `leaf` up to the root,
188
+ * root-first (matching {@link crumbsOf}'s order). Built from the fiber `return`
189
+ * chain for the measure-fallback breadcrumb. Consecutive hosts sharing a
190
+ * `data-pa-loc` (a forwarding wrapper re-emitting the call site) collapse to a
191
+ * single crumb; names come from the babel plugin's `data-pa-comp`. Pure over a
192
+ * fiber-like chain (capped against a malformed `return` cycle); exported for
193
+ * unit testing.
194
+ */
195
+ export declare function taggedAncestors(leaf: FiberLike | null): {
196
+ name: string;
197
+ loc: Loc;
198
+ fiber: FiberLike;
199
+ }[];
140
200
  /**
141
201
  * Resolve a tap (in window coordinates) to a {@link PickResult}.
142
202
  *
@@ -0,0 +1,2 @@
1
+ /** Live soft-keyboard height in px (0 when hidden). */
2
+ export declare function useKeyboardHeight(): number;
@@ -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
- /** Interrupt the in-flight run. */
49
- interrupt(): void;
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",
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": [
@@ -80,8 +80,8 @@
80
80
  "@types/ws": "^8.18.1",
81
81
  "tsdown": "^0.22.0",
82
82
  "typescript": "^6.0.3",
83
- "@pinagent/db": "0.0.1",
84
- "@pinagent/agent-runner": "0.0.0"
83
+ "@pinagent/agent-runner": "0.0.0",
84
+ "@pinagent/db": "0.0.1"
85
85
  },
86
86
  "scripts": {
87
87
  "prebuild": "node scripts/copy-drizzle.mjs",