@pinagent/react-native 0.2.1 → 0.2.3
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/Pinagent.d.ts +50 -0
- package/dist/native/StreamSheet.d.ts +39 -0
- package/dist/native/brand.d.ts +13 -0
- package/dist/native/index.d.ts +7 -0
- package/dist/native/inspector.d.ts +150 -0
- package/dist/native/multi-pick.d.ts +55 -0
- package/dist/native/pin-icon.d.ts +21 -0
- package/dist/native/restore.d.ts +50 -0
- package/dist/native/screenshot.d.ts +1 -0
- package/dist/native/submit-outcome.d.ts +53 -0
- package/dist/native/transcript.d.ts +112 -0
- package/dist/native/transport.d.ts +49 -0
- package/dist/native/types.d.ts +108 -0
- package/dist/native/ws-client.d.ts +54 -0
- package/package.json +7 -5
- package/src/native/inspector.ts +67 -7
- package/src/native/pin-icon.tsx +6 -4
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <Pinagent/> — the React Native widget. Mount it once at your app root:
|
|
3
|
+
*
|
|
4
|
+
* export default function App() {
|
|
5
|
+
* return (
|
|
6
|
+
* <>
|
|
7
|
+
* <YourApp />
|
|
8
|
+
* <Pinagent />
|
|
9
|
+
* </>
|
|
10
|
+
* );
|
|
11
|
+
* }
|
|
12
|
+
*
|
|
13
|
+
* Modeled on pinagent's Next.js `<Pinagent/>` (a single root-mounted
|
|
14
|
+
* component) rather than the Vite `<script>` injection, which has no RN
|
|
15
|
+
* analog. Renders `null` in production so it has zero cost in release
|
|
16
|
+
* builds.
|
|
17
|
+
*
|
|
18
|
+
* Flow: tap the pin FAB to arm picking → tap a view → we resolve its source
|
|
19
|
+
* via the RN Inspector, hide our own overlay, and capture a screenshot →
|
|
20
|
+
* type a comment → submit POSTs to the Metro middleware, which stores it
|
|
21
|
+
* and (optionally) spawns an agent. When an agent is spawned, a live
|
|
22
|
+
* transcript sheet streams the run back over WebSocket (see StreamSheet /
|
|
23
|
+
* ws-client); otherwise a toast confirms the comment was filed for pull-mode
|
|
24
|
+
* (MCP) pickup. "+ Add element" multi-picks several targets into one comment
|
|
25
|
+
* (sent as `additionalAnchors`); a single pick leaves them null.
|
|
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.
|
|
31
|
+
*/
|
|
32
|
+
import type { ReactElement } from 'react';
|
|
33
|
+
export interface PinagentProps {
|
|
34
|
+
/**
|
|
35
|
+
* Absolute project root, used to make `_debugSource` file paths
|
|
36
|
+
* project-relative (matching the web babel plugin's output). Defaults
|
|
37
|
+
* to the value Metro injects, falling back to '' (paths stay absolute,
|
|
38
|
+
* still usable).
|
|
39
|
+
*/
|
|
40
|
+
projectRoot?: string;
|
|
41
|
+
/** Route/screen name to record with the comment. Defaults to OS name. */
|
|
42
|
+
screenName?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Hard dev-only gate. `__DEV__` is `false` in release bundles, so the
|
|
46
|
+
* whole widget — and its require()s into RN internals — drops out. Kept
|
|
47
|
+
* as a thin wrapper so the hooks live in `PinagentDev`, called
|
|
48
|
+
* unconditionally (rules-of-hooks).
|
|
49
|
+
*/
|
|
50
|
+
export declare function Pinagent(props: PinagentProps): ReactElement | null;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live agent transcript sheet for React Native.
|
|
3
|
+
*
|
|
4
|
+
* After a comment is submitted and an agent is spawned, the widget opens this
|
|
5
|
+
* bottom sheet and streams the run over WebSocket (see `ws-client.ts`). It's
|
|
6
|
+
* the RN analog of the web widget's agent tray: a scrolling transcript, an
|
|
7
|
+
* answer form when the agent calls `ask_user`, a follow-up box, and Stop /
|
|
8
|
+
* Dismiss controls.
|
|
9
|
+
*
|
|
10
|
+
* State is intentionally simple and reducer-driven: agent events accumulate in
|
|
11
|
+
* one array folded by `renderTranscript`; user follow-ups are tracked locally
|
|
12
|
+
* (the bus only streams agent events, not the developer's messages). A
|
|
13
|
+
* reconnect replays the agent transcript, so we clear `events` on `onReset`
|
|
14
|
+
* but keep local follow-ups.
|
|
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.
|
|
22
|
+
*/
|
|
23
|
+
import type { ReactElement } from 'react';
|
|
24
|
+
export interface StreamSheetProps {
|
|
25
|
+
feedbackId: string;
|
|
26
|
+
/** Source label shown in the header (e.g. `file:line` or component name). */
|
|
27
|
+
target: string;
|
|
28
|
+
/** Render as a compact pill (WS stays live) instead of the full sheet. */
|
|
29
|
+
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
|
+
onMinimize: () => void;
|
|
34
|
+
/** Expand the pill back to the full sheet. */
|
|
35
|
+
onExpand: () => void;
|
|
36
|
+
/** Dismiss for good — tears down the WS and removes this run's view. */
|
|
37
|
+
onClose: () => void;
|
|
38
|
+
}
|
|
39
|
+
export declare function StreamSheet({ feedbackId, target, minimized, stackIndex, onMinimize, onExpand, onClose, }: StreamSheetProps): ReactElement;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pinagent brand palette for the React Native widget.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors BRAND_INK / BRAND_CREAM / BRAND_GOLD in `packages/ui/src/tokens.ts`. That
|
|
5
|
+
* package is web-only (React-DOM + Tailwind) so the native widget can't import
|
|
6
|
+
* it — keep these in sync if the brand palette changes there.
|
|
7
|
+
*/
|
|
8
|
+
/** Dark charcoal — the FAB surface / ink-mode surfaces and primary text. */
|
|
9
|
+
export declare const BRAND_INK = "#201B21";
|
|
10
|
+
/** Warm cream — the pin mark on dark surfaces, light backgrounds, brand identity. */
|
|
11
|
+
export declare const BRAND_CREAM = "#FCF9E8";
|
|
12
|
+
/** Gold accent — focus rings; the active picking state. */
|
|
13
|
+
export declare const BRAND_GOLD = "#FFD700";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { PinagentProps } from './Pinagent';
|
|
2
|
+
export { Pinagent } from './Pinagent';
|
|
3
|
+
export type { AgentEvent, ServerMessage, TranscriptRow } from './transcript';
|
|
4
|
+
export { pendingAsk, renderTranscript } from './transcript';
|
|
5
|
+
export { devServerBaseUrl, submitFeedback } from './transport';
|
|
6
|
+
export type { FeedbackInput, PickResult } from './types';
|
|
7
|
+
export { devServerWsUrl, StreamClient } from './ws-client';
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tap point → source location, the React Native way.
|
|
3
|
+
*
|
|
4
|
+
* This is the RN analog of the web widget's DOM walk for `data-pa-loc`.
|
|
5
|
+
* RN's own dev Inspector (Dev Menu → "Show Inspector") resolves a touch
|
|
6
|
+
* to a component + source file using exactly this internal API; we lean
|
|
7
|
+
* on the same machinery rather than reinventing it.
|
|
8
|
+
*
|
|
9
|
+
* Source data comes from the `data-pa-loc="file:line:col"` prop the
|
|
10
|
+
* `@pinagent/react-native/babel` plugin splices onto every authored JSX
|
|
11
|
+
* element at build time — the exact RN analog of the web babel plugin's
|
|
12
|
+
* DOM attribute. The plugin's prop rides along on the host fiber's
|
|
13
|
+
* `memoizedProps`, which `getInspectorDataForViewAtPoint` hands back to us
|
|
14
|
+
* as `data.props`, so the tapped view resolves to its source directly.
|
|
15
|
+
*
|
|
16
|
+
* Why a build-time prop instead of RN's old `_debugSource`: React 19
|
|
17
|
+
* deleted `_debugSource`, and RN 0.81+ dropped the `source` field from the
|
|
18
|
+
* inspector payload — neither carries a source location anymore. We still
|
|
19
|
+
* read both as a fallback for older RN/React, then degrade to `loc: null`.
|
|
20
|
+
*
|
|
21
|
+
* Both the module path AND the payload shape returned by
|
|
22
|
+
* `getInspectorDataForViewAtPoint` have shifted across RN versions, so
|
|
23
|
+
* every read here is defensive: we extract what we can and degrade to
|
|
24
|
+
* `loc: null` rather than throw inside a tap handler.
|
|
25
|
+
*/
|
|
26
|
+
import type { PickResult } from './types';
|
|
27
|
+
interface RawFiberLike {
|
|
28
|
+
fileName?: string;
|
|
29
|
+
lineNumber?: number;
|
|
30
|
+
columnNumber?: number;
|
|
31
|
+
}
|
|
32
|
+
type RawProps = Record<string, unknown> | null | undefined;
|
|
33
|
+
/** RN measure callback: `(x, y, width, height, pageX, pageY)`. */
|
|
34
|
+
type RawMeasureCb = (x: number, y: number, width: number, height: number, pageX: number, pageY: number) => void;
|
|
35
|
+
interface RawHierarchyItem {
|
|
36
|
+
name?: string;
|
|
37
|
+
getInspectorData?: (toFiber: unknown) => {
|
|
38
|
+
/** Removed in RN 0.81+, kept for back-compat. */
|
|
39
|
+
source?: RawFiberLike | null;
|
|
40
|
+
/** The host fiber's `memoizedProps` — carries our `data-pa-loc`. */
|
|
41
|
+
props?: RawProps;
|
|
42
|
+
/** Measures this owner's host fiber (window/screen coords). */
|
|
43
|
+
measure?: (cb: RawMeasureCb) => void;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
interface RawInspectorData {
|
|
47
|
+
frame?: {
|
|
48
|
+
left: number;
|
|
49
|
+
top: number;
|
|
50
|
+
width: number;
|
|
51
|
+
height: number;
|
|
52
|
+
} | null;
|
|
53
|
+
hierarchy?: RawHierarchyItem[];
|
|
54
|
+
/** Newer RN returns the closest fiber directly. */
|
|
55
|
+
closestInstance?: {
|
|
56
|
+
_debugSource?: RawFiberLike;
|
|
57
|
+
} | null;
|
|
58
|
+
/** Some versions surface the resolved source straight on the payload. */
|
|
59
|
+
source?: RawFiberLike | null;
|
|
60
|
+
/**
|
|
61
|
+
* NOT the tapped leaf's props. RN fills this from the nearest authored
|
|
62
|
+
* *composite owner's* FIRST host descendant (`getHostProps` →
|
|
63
|
+
* `findCurrentHostFiber`) — i.e. that component's outermost view — so a tap
|
|
64
|
+
* on a nested child surfaces its container here, not the element under the
|
|
65
|
+
* finger. Used only as a fallback; `closestPublicInstance` pins the leaf.
|
|
66
|
+
*/
|
|
67
|
+
props?: RawProps;
|
|
68
|
+
/**
|
|
69
|
+
* The public instance of the host view actually under the finger — the
|
|
70
|
+
* deepest hit-tested node. Fabric only; Paper passes a numeric view tag we
|
|
71
|
+
* can't bridge, so we degrade to `props`. Its fiber's `memoizedProps` carry
|
|
72
|
+
* the leaf's own `data-pa-loc`. See {@link tappedLeafLoc}.
|
|
73
|
+
*/
|
|
74
|
+
closestPublicInstance?: unknown;
|
|
75
|
+
}
|
|
76
|
+
interface FiberLike {
|
|
77
|
+
tag?: number;
|
|
78
|
+
return?: FiberLike | null;
|
|
79
|
+
stateNode?: {
|
|
80
|
+
canonical?: {
|
|
81
|
+
publicInstance?: unknown;
|
|
82
|
+
};
|
|
83
|
+
} | null;
|
|
84
|
+
/** Host fibers carry the committed props — where `data-pa-loc` rides. */
|
|
85
|
+
memoizedProps?: RawProps;
|
|
86
|
+
}
|
|
87
|
+
type Loc = NonNullable<PickResult['loc']>;
|
|
88
|
+
/**
|
|
89
|
+
* Walk a host fiber's render-tree parent (`return`) chain, returning the first
|
|
90
|
+
* `data-pa-loc` found on a fiber's `memoizedProps` — the tapped element itself,
|
|
91
|
+
* or, when that exact host is untagged (a 3rd-party / RN-internal view), the
|
|
92
|
+
* nearest authored element enclosing it. Capped against a malformed `return`
|
|
93
|
+
* cycle. Pure over a fiber-like chain; exported for unit testing without an RN
|
|
94
|
+
* runtime.
|
|
95
|
+
*/
|
|
96
|
+
export declare function nearestPaLocUp(fiber: FiberLike | null): Loc | null;
|
|
97
|
+
/**
|
|
98
|
+
* Keep only authored React components in the breadcrumb. Two kinds of noise
|
|
99
|
+
* are hidden because clicking them is meaningless — they map to no source the
|
|
100
|
+
* developer can act on:
|
|
101
|
+
*
|
|
102
|
+
* - **Native host components** — RN's view classes, named `RCT…`
|
|
103
|
+
* (`RCTText`, `RCTView`, `RCTScrollView`, …).
|
|
104
|
+
* - **HOC / wrapper display names** — parenthesized by convention
|
|
105
|
+
* (`withDevTools(App)`, `ForwardRef(X)`, `Memo(X)`, `Connect(X)`).
|
|
106
|
+
*
|
|
107
|
+
* Identifiers can't contain `(`, so a parenthesized name is always a wrapper.
|
|
108
|
+
*/
|
|
109
|
+
export declare function isAuthoredComponentName(name: unknown): name is string;
|
|
110
|
+
type Frame = NonNullable<PickResult['frame']>;
|
|
111
|
+
interface RawCrumb {
|
|
112
|
+
name: string;
|
|
113
|
+
loc: Loc | null;
|
|
114
|
+
measure?: (cb: RawMeasureCb) => void;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Build the per-segment breadcrumb: each authored component in the hierarchy
|
|
118
|
+
* paired with its own `data-pa-loc` (so a press can re-anchor onto that
|
|
119
|
+
* ancestor) and a `measure` fn (so the highlight can follow the selection).
|
|
120
|
+
* Same order as {@link nameChainOf} — root first, tapped last.
|
|
121
|
+
*
|
|
122
|
+
* Each crumb's `loc` falls back to the nearest resolvable source in the
|
|
123
|
+
* hierarchy when its own host props carry none, so pressing ANY breadcrumb
|
|
124
|
+
* re-anchors the comment onto a real `file:line` — not just the tapped
|
|
125
|
+
* (innermost) one. Without the fallback, only the tapped element (which
|
|
126
|
+
* `pickLoc` resolves with its own hierarchy walk) got a path; ancestor crumbs
|
|
127
|
+
* whose first host child is untagged collapsed to `loc: null`, so re-focusing
|
|
128
|
+
* onto them showed a bare component name instead of a source snippet.
|
|
129
|
+
*
|
|
130
|
+
* Exported for unit testing — a pure function over the (duck-typed) inspector
|
|
131
|
+
* payload, so it needs no RN runtime (unlike `resolvePick`).
|
|
132
|
+
*/
|
|
133
|
+
export declare function crumbsOf(data: RawInspectorData): RawCrumb[];
|
|
134
|
+
/**
|
|
135
|
+
* Measure a hierarchy item's host fiber to a window-coordinate {@link Frame}.
|
|
136
|
+
* Resolves null if there's no measure fn or it never calls back (guarded so a
|
|
137
|
+
* stuck measure can't hang the pick).
|
|
138
|
+
*/
|
|
139
|
+
export declare function measureFrame(measure?: (cb: RawMeasureCb) => void): Promise<Frame | null>;
|
|
140
|
+
/**
|
|
141
|
+
* Resolve a tap (in window coordinates) to a {@link PickResult}.
|
|
142
|
+
*
|
|
143
|
+
* @param rootView A host view instance from which to reach the app root —
|
|
144
|
+
* pass a ref's `.current` (the widget passes its own overlay `<View>`).
|
|
145
|
+
* We climb to the app-root host instance before hit-testing, since the
|
|
146
|
+
* inspector searches within the passed view's subtree. NOT a
|
|
147
|
+
* `findNodeHandle` number — the Fabric inspector rejects a bare tag.
|
|
148
|
+
*/
|
|
149
|
+
export declare function resolvePick(rootView: unknown, x: number, y: number, projectRoot: string): Promise<PickResult>;
|
|
150
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-select payload builder (ticket 008).
|
|
3
|
+
*
|
|
4
|
+
* On web, Cmd/Ctrl-click accumulates several elements into one comment and the
|
|
5
|
+
* extras are submitted as `additionalAnchors` (landing in the
|
|
6
|
+
* `widget_anchors.additional_anchors` JSON column). RN is touch-only, so the
|
|
7
|
+
* composer offers an explicit "+ Add element" affordance: re-enter pick mode,
|
|
8
|
+
* tap another element, and it appends a removable chip. This module turns the
|
|
9
|
+
* primary anchor + the collected extras into the `additionalAnchors` array,
|
|
10
|
+
* matching the web `AdditionalAnchorSchema` shape so the server accepts it
|
|
11
|
+
* unchanged.
|
|
12
|
+
*
|
|
13
|
+
* Pure (no RN runtime imports) → unit-testable here. The RN UI state + capture
|
|
14
|
+
* lives in Pinagent.tsx; this just shapes the wire payload.
|
|
15
|
+
*
|
|
16
|
+
* Web semantics preserved:
|
|
17
|
+
* - A single pick (no extras) → `additionalAnchors` OMITTED (the server stores
|
|
18
|
+
* `additional_anchors` as null), not an empty array.
|
|
19
|
+
* - The primary anchor is NOT duplicated into the extras.
|
|
20
|
+
* - Each extra keeps the loc/selector it was tapped with (no breadcrumb
|
|
21
|
+
* re-anchoring for extras — that applies to the primary only, web parity).
|
|
22
|
+
*/
|
|
23
|
+
import type { AdditionalAnchor } from './types';
|
|
24
|
+
/** A primary or extra pick as captured by the RN composer. */
|
|
25
|
+
export interface ChipPick {
|
|
26
|
+
/** Stable key for the chip + removal (e.g. a per-pick counter). */
|
|
27
|
+
key: string;
|
|
28
|
+
/** Resolved source location, or null for an unresolvable native view. */
|
|
29
|
+
loc: {
|
|
30
|
+
file: string;
|
|
31
|
+
line: number;
|
|
32
|
+
col: number;
|
|
33
|
+
} | null;
|
|
34
|
+
/** Component name-chain ("App > Home > Button") — RN's selector stand-in. */
|
|
35
|
+
selector: string;
|
|
36
|
+
/** Tap point in window coordinates (the `clickX`/`clickY` the schema wants). */
|
|
37
|
+
clickX: number;
|
|
38
|
+
clickY: number;
|
|
39
|
+
/** Innermost component name, for the chip label. */
|
|
40
|
+
label: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build the `additionalAnchors` field from the extra picks (everything after
|
|
44
|
+
* the primary). Returns `undefined` when there are no extras so the caller can
|
|
45
|
+
* spread it and leave the field off entirely for single-pick submits.
|
|
46
|
+
*
|
|
47
|
+
* @param extras the non-primary picks, in pick order (preserved on the wire).
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildAdditionalAnchors(extras: readonly ChipPick[]): AdditionalAnchor[] | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Remove a chip by key from a pick list, preserving order. Used for both the
|
|
52
|
+
* primary+extras chip row removal and the extras-only list; the caller decides
|
|
53
|
+
* which list to pass (the primary is never removable in the UI).
|
|
54
|
+
*/
|
|
55
|
+
export declare function removeChip(picks: readonly ChipPick[], key: string): ChipPick[];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PinIcon — pinagent's teardrop pin mark, for the React Native FAB.
|
|
3
|
+
*
|
|
4
|
+
* The mark is the canonical PIN_PATH / BRAND_VIEWBOX from `packages/ui/src/tokens.ts`
|
|
5
|
+
* (web-only, hence mirrored here). We draw it with `react-native-svg` when it's
|
|
6
|
+
* available — an OPTIONAL peer, lazily required exactly like
|
|
7
|
+
* `react-native-view-shot` in screenshot.ts, so a release build never pulls the
|
|
8
|
+
* native module in: the require only runs once <Pinagent/> actually renders,
|
|
9
|
+
* which it never does when `!__DEV__`.
|
|
10
|
+
*
|
|
11
|
+
* When the peer isn't installed we fall back to a View-drawn teardrop in the
|
|
12
|
+
* same colour, so the FAB always shows a brand pin — never a generic glyph.
|
|
13
|
+
*/
|
|
14
|
+
import type { ReactElement } from 'react';
|
|
15
|
+
export interface PinIconProps {
|
|
16
|
+
/** Square edge length, in px. */
|
|
17
|
+
size?: number;
|
|
18
|
+
/** Fill colour of the pin (pass a brand token). */
|
|
19
|
+
color: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function PinIcon({ size, color }: PinIconProps): ReactElement;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Restore-filter: turn the server's feedback list into the minimized pills the
|
|
3
|
+
* RN widget seeds on mount, so an app reload mid-run brings the running streams
|
|
4
|
+
* back instead of losing them.
|
|
5
|
+
*
|
|
6
|
+
* The dev server (`.pinagent/db.sqlite`) is the source of truth — RN keeps no
|
|
7
|
+
* device-local mirror (see ticket 001). On `<Pinagent/>` mount we
|
|
8
|
+
* `GET /__pinagent/feedback`, run the list through {@link restorePills}, and
|
|
9
|
+
* seed `streams` with the result; each restored id then subscribes over the
|
|
10
|
+
* existing WS client, which replays the transcript (and fires `done` for
|
|
11
|
+
* already-finished runs).
|
|
12
|
+
*
|
|
13
|
+
* This module is pure (no RN runtime imports) so it's unit-testable here. The
|
|
14
|
+
* filter mirrors the web widget's `listPendingForCurrentPage`
|
|
15
|
+
* (`packages/widget/src/db/reads.ts`): pending-only, scoped to the current
|
|
16
|
+
* surface URL, newest first.
|
|
17
|
+
*/
|
|
18
|
+
/** Default cap so a stale backlog doesn't flood the screen with pills. */
|
|
19
|
+
export declare const RESTORE_LIMIT = 5;
|
|
20
|
+
/** A minimized run pill: the same shape `<Pinagent/>` keeps in `streams`. */
|
|
21
|
+
export interface RestoredPill {
|
|
22
|
+
id: string;
|
|
23
|
+
/** Header label — `file:line` if anchored, else the selector/component. */
|
|
24
|
+
target: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The subset of a feedback list item (`storage.list()` projection,
|
|
28
|
+
* `FeedbackRecord`) this filter reads. Loosely typed so it tolerates the wire
|
|
29
|
+
* JSON without importing the agent-runner type into RN source.
|
|
30
|
+
*/
|
|
31
|
+
export interface RestoreCandidate {
|
|
32
|
+
id?: unknown;
|
|
33
|
+
status?: unknown;
|
|
34
|
+
url?: unknown;
|
|
35
|
+
file?: unknown;
|
|
36
|
+
line?: unknown;
|
|
37
|
+
selector?: unknown;
|
|
38
|
+
updatedAt?: unknown;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Filter a feedback list to the pills worth restoring on this surface:
|
|
42
|
+
*
|
|
43
|
+
* - `status === 'pending'` — resolved/dismissed runs don't come back (web parity).
|
|
44
|
+
* - `url === surfaceUrl` — RN submits `url: screenName ?? Platform.OS`; a run
|
|
45
|
+
* started on a different screen would restore at meaningless coordinates.
|
|
46
|
+
* - newest first by `updatedAt`, capped at `limit` (default {@link RESTORE_LIMIT}).
|
|
47
|
+
*
|
|
48
|
+
* Items missing an `id` are dropped (can't subscribe to them).
|
|
49
|
+
*/
|
|
50
|
+
export declare function restorePills(items: readonly RestoreCandidate[] | null | undefined, surfaceUrl: string, limit?: number): RestoredPill[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function captureScreenshot(): Promise<string>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure submit-outcome reducer (ticket 002).
|
|
3
|
+
*
|
|
4
|
+
* `onSubmit` in Pinagent.tsx used to clear the composer (comment, pick,
|
|
5
|
+
* screenshot) UNCONDITIONALLY after `submitFeedback()` returned — so a Metro
|
|
6
|
+
* restart, a network blip, or a release build at the moment of submit threw
|
|
7
|
+
* away the typed comment, the picked anchor, and the screenshot, leaving the
|
|
8
|
+
* user with a 2.5s toast and a blank composer.
|
|
9
|
+
*
|
|
10
|
+
* This module computes the next composer state from a `SubmitResult` so the
|
|
11
|
+
* "keep the draft on failure, clear only on success" rule is data, not buried
|
|
12
|
+
* UI flow — and is unit-testable here (the RN UI itself is not). Mapping the
|
|
13
|
+
* outcome to React state lives in `onSubmit`; this just decides the shape.
|
|
14
|
+
*/
|
|
15
|
+
/** The relevant submit result fields (a subset of transport's `SubmitResult`). */
|
|
16
|
+
export interface SubmitOutcomeInput {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
id?: string;
|
|
19
|
+
agentSpawned?: boolean;
|
|
20
|
+
error?: string;
|
|
21
|
+
}
|
|
22
|
+
/** What the composer should do next, given a submit result. */
|
|
23
|
+
export interface SubmitOutcome {
|
|
24
|
+
/**
|
|
25
|
+
* `'clear'` — wipe the composer (comment/pick/shot) and go idle (success).
|
|
26
|
+
* `'keep'` — retain the composer and surface the error inline + offer Retry.
|
|
27
|
+
*/
|
|
28
|
+
composer: 'clear' | 'keep';
|
|
29
|
+
/** Inline error to show under the composer when `composer === 'keep'`. */
|
|
30
|
+
error: string | null;
|
|
31
|
+
/**
|
|
32
|
+
* When a run was spawned, the id to open as a live stream. Null when nothing
|
|
33
|
+
* to stream (spawn off, or a failed submit).
|
|
34
|
+
*/
|
|
35
|
+
streamId: string | null;
|
|
36
|
+
/**
|
|
37
|
+
* Transient toast text for the non-streaming success/failure paths, or null
|
|
38
|
+
* when the outcome opens a stream instead (which has its own UI). Kept for a
|
|
39
|
+
* filed-for-pull-mode confirmation; failures show inline, not a toast.
|
|
40
|
+
*/
|
|
41
|
+
toast: string | null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Decide the next composer state from a submit result.
|
|
45
|
+
*
|
|
46
|
+
* - Failure (`ok === false`): keep the composer + its draft, surface the error
|
|
47
|
+
* inline (never a vanishing toast), no stream, no clear. Retry re-submits the
|
|
48
|
+
* retained payload.
|
|
49
|
+
* - Success with a spawned agent: clear the composer and open the stream.
|
|
50
|
+
* - Success without a spawned agent (spawn off / pull mode): clear the composer
|
|
51
|
+
* and show a transient "Sent" toast.
|
|
52
|
+
*/
|
|
53
|
+
export declare function submitOutcome(result: SubmitOutcomeInput): SubmitOutcome;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript reducer + wire types for the RN widget.
|
|
3
|
+
*
|
|
4
|
+
* Deliberate, dependency-free mirror of `@pinagent/shared`'s `AgentEvent`
|
|
5
|
+
* union (event-bus.ts), `ServerMessage` (ws-protocol.ts), and the canonical
|
|
6
|
+
* `renderTranscript` (render-transcript.ts). We DON'T import the package: the
|
|
7
|
+
* native client ships as **source** and is bundled onto the device by the
|
|
8
|
+
* consumer's Metro, but `@pinagent/shared` is `private` (unpublished) and
|
|
9
|
+
* built for Node — importing it into device code would break a real
|
|
10
|
+
* `npm install @pinagent/react-native`, the same way bundling any unpublished
|
|
11
|
+
* `@pinagent/*` dep does. The package's `./server` entry CAN depend on shared
|
|
12
|
+
* (tsdown bundles it into dist); device source cannot.
|
|
13
|
+
*
|
|
14
|
+
* Keep in sync with those three files. The shapes are stable and the reducer
|
|
15
|
+
* mirrors the shared one, extended with the streaming-only kinds (ask/status)
|
|
16
|
+
* the interactive RN sheet renders.
|
|
17
|
+
*/
|
|
18
|
+
/** Flat AgentEvent union — mirror of `@pinagent/shared`'s `event-bus.ts`. */
|
|
19
|
+
export type AgentEvent = {
|
|
20
|
+
type: 'init';
|
|
21
|
+
sessionId: string;
|
|
22
|
+
model: string;
|
|
23
|
+
permissionMode: string;
|
|
24
|
+
apiKeySource: string;
|
|
25
|
+
} | {
|
|
26
|
+
type: 'text';
|
|
27
|
+
text: string;
|
|
28
|
+
} | {
|
|
29
|
+
type: 'tool_use';
|
|
30
|
+
name: string;
|
|
31
|
+
summary: string;
|
|
32
|
+
} | {
|
|
33
|
+
type: 'tool_result';
|
|
34
|
+
ok: boolean;
|
|
35
|
+
} | {
|
|
36
|
+
type: 'progress';
|
|
37
|
+
turn: number;
|
|
38
|
+
} | {
|
|
39
|
+
type: 'ask_user';
|
|
40
|
+
askId: string;
|
|
41
|
+
question: string;
|
|
42
|
+
context?: string;
|
|
43
|
+
options?: string[];
|
|
44
|
+
} | {
|
|
45
|
+
type: 'error';
|
|
46
|
+
message: string;
|
|
47
|
+
} | {
|
|
48
|
+
type: 'result';
|
|
49
|
+
subtype: string;
|
|
50
|
+
numTurns: number;
|
|
51
|
+
totalCostUsd: number;
|
|
52
|
+
durationMs: number;
|
|
53
|
+
} | {
|
|
54
|
+
type: 'status_changed';
|
|
55
|
+
status: 'pending' | 'fixed' | 'wontfix' | 'deferred';
|
|
56
|
+
note: string | null;
|
|
57
|
+
commitSha: string | null;
|
|
58
|
+
resolvedAt: string | null;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Server → client frames the RN client acts on (subset of the web protocol).
|
|
62
|
+
*
|
|
63
|
+
* Only the frames the widget handles are modeled. Other frames (project /
|
|
64
|
+
* extension fan-out, pong) still arrive on the wire — `StreamClient.onMessage`
|
|
65
|
+
* casts the parsed JSON to this type and its `switch` ignores any `type` it
|
|
66
|
+
* doesn't handle. We deliberately avoid an open
|
|
67
|
+
* `{ type: string; [k: string]: unknown }` member: it overlaps every
|
|
68
|
+
* discriminant, collapsing `event`/`message` to `{}` at the use sites and
|
|
69
|
+
* defeating narrowing.
|
|
70
|
+
*/
|
|
71
|
+
export type ServerMessage = {
|
|
72
|
+
type: 'event';
|
|
73
|
+
feedbackId: string;
|
|
74
|
+
event: AgentEvent;
|
|
75
|
+
} | {
|
|
76
|
+
type: 'done';
|
|
77
|
+
feedbackId: string;
|
|
78
|
+
} | {
|
|
79
|
+
type: 'error';
|
|
80
|
+
feedbackId?: string;
|
|
81
|
+
message: string;
|
|
82
|
+
} | {
|
|
83
|
+
type: 'worktree_state';
|
|
84
|
+
feedbackId: string;
|
|
85
|
+
state: string;
|
|
86
|
+
commitSha?: string;
|
|
87
|
+
};
|
|
88
|
+
export type TranscriptKind = 'text' | 'tool' | 'error' | 'result' | 'ask' | 'status';
|
|
89
|
+
export interface TranscriptRow {
|
|
90
|
+
/** Stable key for React lists; derived from event index. */
|
|
91
|
+
id: string;
|
|
92
|
+
kind: TranscriptKind;
|
|
93
|
+
/** Primary text. For tools, the tool name. */
|
|
94
|
+
text: string;
|
|
95
|
+
/** Tool argument summary / ask options, when present. */
|
|
96
|
+
detail?: string;
|
|
97
|
+
/** tool_result success flag → ✓/✗ marker. */
|
|
98
|
+
ok?: boolean;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Fold AgentEvents into render-ready rows. Pure and deterministic — mirrors
|
|
102
|
+
* the shared `renderTranscript`, plus `ask_user` / `status_changed` rows the
|
|
103
|
+
* interactive RN sheet shows. `init`, `progress`, `tool_result` produce no row
|
|
104
|
+
* of their own (`tool_result` annotates the preceding tool row with ✓/✗).
|
|
105
|
+
*/
|
|
106
|
+
export declare function renderTranscript(events: AgentEvent[]): TranscriptRow[];
|
|
107
|
+
/** The latest unanswered ask_user, if the run is currently blocked on one. */
|
|
108
|
+
export declare function pendingAsk(events: AgentEvent[]): {
|
|
109
|
+
askId: string;
|
|
110
|
+
question: string;
|
|
111
|
+
options: string[];
|
|
112
|
+
} | null;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { RestoreCandidate } from './restore';
|
|
2
|
+
import type { FeedbackInput } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Parse `http://192.168.1.5:8081/index.bundle?...` (or the RN packager's
|
|
5
|
+
* variants) down to `http://192.168.1.5:8081`. Returns null in release builds,
|
|
6
|
+
* where no Metro server is reachable.
|
|
7
|
+
*
|
|
8
|
+
* Prefers RN's `getDevServer()` (TurboModule-backed, works under the New
|
|
9
|
+
* Architecture) and falls back to the legacy `NativeModules.SourceCode.scriptURL`
|
|
10
|
+
* for pre-bridgeless RN. The legacy proxy is empty under bridgeless, which is
|
|
11
|
+
* what made every submit fail with "No dev server" on RN 0.82+.
|
|
12
|
+
*/
|
|
13
|
+
export declare function devServerBaseUrl(): string | null;
|
|
14
|
+
export interface SubmitResult {
|
|
15
|
+
ok: boolean;
|
|
16
|
+
id?: string;
|
|
17
|
+
agentSpawned?: boolean;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Send one comment. Mirrors the web widget's POST to
|
|
22
|
+
* `/__pinagent/feedback`; the response (`{ id, agentSpawned }`) is the
|
|
23
|
+
* same one the Vite/Next middleware returns.
|
|
24
|
+
*/
|
|
25
|
+
export declare function submitFeedback(input: FeedbackInput): Promise<SubmitResult>;
|
|
26
|
+
/**
|
|
27
|
+
* Fetch the conversation list from the dev server (`GET /__pinagent/feedback`).
|
|
28
|
+
* Used on `<Pinagent/>` mount to restore minimized pills after an app reload —
|
|
29
|
+
* the server's `.pinagent/db.sqlite` is the source of truth, so RN keeps no
|
|
30
|
+
* device-local mirror. Returns `[]` (degrade silently) when the dev server is
|
|
31
|
+
* unreachable or the request fails — exactly as today when there's no server.
|
|
32
|
+
*
|
|
33
|
+
* The items are the `storage.list()` projection (`FeedbackRecord[]`); the
|
|
34
|
+
* caller filters them with `restorePills`. Typed loosely here so the wire JSON
|
|
35
|
+
* doesn't drag the agent-runner type into RN source.
|
|
36
|
+
*/
|
|
37
|
+
export declare function fetchFeedbackList(): Promise<RestoreCandidate[]>;
|
|
38
|
+
/**
|
|
39
|
+
* Ask the dev server to open a source location in the editor on the machine
|
|
40
|
+
* running Metro — the RN analog of the web composer's "navigate to file".
|
|
41
|
+
* Fire-and-forget: the device gets no useful signal beyond "request sent".
|
|
42
|
+
*/
|
|
43
|
+
export declare function openInEditor(loc: {
|
|
44
|
+
file: string;
|
|
45
|
+
line: number;
|
|
46
|
+
col: number;
|
|
47
|
+
}): Promise<boolean>;
|
|
48
|
+
/** `${Platform.OS} ${Platform.Version}` — RN's stand-in for a UA string. */
|
|
49
|
+
export declare function platformTag(): string;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire shape the RN widget produces. It mirrors `FeedbackInputSchema`
|
|
3
|
+
* in `packages/agent-runner/src/storage.ts` so the existing server
|
|
4
|
+
* accepts a phone-filed comment with zero backend changes. Keep this in
|
|
5
|
+
* lockstep with that zod schema — the middleware re-validates against it.
|
|
6
|
+
*/
|
|
7
|
+
export interface FeedbackInput {
|
|
8
|
+
/** Comment text the developer typed. 1..8000 chars (server-enforced). */
|
|
9
|
+
comment: string;
|
|
10
|
+
/**
|
|
11
|
+
* Source location of the tapped component, from the fiber's
|
|
12
|
+
* `_debugSource` (the RN analog of web's `data-pa-loc`). Null when the
|
|
13
|
+
* tapped view has no resolvable source (a deep native view with no
|
|
14
|
+
* composite owner in dev).
|
|
15
|
+
*/
|
|
16
|
+
loc: {
|
|
17
|
+
file: string;
|
|
18
|
+
line: number;
|
|
19
|
+
col: number;
|
|
20
|
+
} | null;
|
|
21
|
+
/**
|
|
22
|
+
* Web sends a CSS selector here for HMR re-anchoring. RN has no
|
|
23
|
+
* selectors, so v1 sends the component display-name chain (e.g.
|
|
24
|
+
* "App > HomeScreen > PrimaryButton") purely to satisfy the schema and
|
|
25
|
+
* give the agent a human-readable hint. See the design doc's
|
|
26
|
+
* "Deliberate cuts for v1".
|
|
27
|
+
*/
|
|
28
|
+
selector: string;
|
|
29
|
+
/** The current route/screen name (web sends the page URL). */
|
|
30
|
+
url: string;
|
|
31
|
+
/** Window dimensions at pick time. */
|
|
32
|
+
viewport: {
|
|
33
|
+
w: number;
|
|
34
|
+
h: number;
|
|
35
|
+
};
|
|
36
|
+
/** `${Platform.OS} ${Platform.Version}` — the RN analog of a UA string. */
|
|
37
|
+
userAgent: string;
|
|
38
|
+
/** base64 PNG (no data: prefix). Capped at 5MB by the middleware. */
|
|
39
|
+
screenshot: string;
|
|
40
|
+
/** ISO timestamp. */
|
|
41
|
+
createdAt: string;
|
|
42
|
+
/**
|
|
43
|
+
* Extra elements the developer multi-picked into the SAME comment (the
|
|
44
|
+
* 2nd…Nth taps). Optional — omitted entirely for a single pick, exactly
|
|
45
|
+
* like the web widget (the server stores `additional_anchors` as null
|
|
46
|
+
* unless this is a non-empty array). The agent receives them as
|
|
47
|
+
* `additionalTargets` and addresses every location. Same per-anchor shape
|
|
48
|
+
* the web sends in `FeedbackInputSchema.additionalAnchors`
|
|
49
|
+
* (`packages/agent-runner/src/storage.ts`).
|
|
50
|
+
*/
|
|
51
|
+
additionalAnchors?: AdditionalAnchor[];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* One extra (non-primary) pick. Matches the web `AdditionalAnchorSchema`
|
|
55
|
+
* exactly so the server accepts it unchanged: `clickX`/`clickY` are required
|
|
56
|
+
* integers (the tap point in window coordinates); `component` is optional.
|
|
57
|
+
*/
|
|
58
|
+
export interface AdditionalAnchor {
|
|
59
|
+
file: string | null;
|
|
60
|
+
line: number | null;
|
|
61
|
+
col: number | null;
|
|
62
|
+
selector: string;
|
|
63
|
+
clickX: number;
|
|
64
|
+
clickY: number;
|
|
65
|
+
component?: string | null;
|
|
66
|
+
}
|
|
67
|
+
/** One segment of the component breadcrumb. */
|
|
68
|
+
export interface PickCrumb {
|
|
69
|
+
/** Component display name (e.g. `FeatureCard`). */
|
|
70
|
+
name: string;
|
|
71
|
+
/**
|
|
72
|
+
* Source location of that component, from its own `data-pa-loc`. Null when
|
|
73
|
+
* the component carries no resolvable source (e.g. an untagged 3rd-party
|
|
74
|
+
* wrapper). Lets the breadcrumb re-anchor the comment onto an ancestor.
|
|
75
|
+
*/
|
|
76
|
+
loc: FeedbackInput['loc'];
|
|
77
|
+
/**
|
|
78
|
+
* Highlight rectangle for this component (window coordinates), so pressing
|
|
79
|
+
* the crumb moves the on-screen selection outline onto it. Null when it
|
|
80
|
+
* couldn't be measured.
|
|
81
|
+
*/
|
|
82
|
+
frame: {
|
|
83
|
+
x: number;
|
|
84
|
+
y: number;
|
|
85
|
+
width: number;
|
|
86
|
+
height: number;
|
|
87
|
+
} | null;
|
|
88
|
+
}
|
|
89
|
+
/** Result of resolving a tap point to a source location. */
|
|
90
|
+
export interface PickResult {
|
|
91
|
+
/** The precise tapped element's source location (the default target). */
|
|
92
|
+
loc: FeedbackInput['loc'];
|
|
93
|
+
/** Component display-name breadcrumb, newest (tapped) last. */
|
|
94
|
+
nameChain: string[];
|
|
95
|
+
/**
|
|
96
|
+
* Per-segment breadcrumb (same order as {@link nameChain}: root first,
|
|
97
|
+
* tapped component last), each carrying its own `loc` so pressing a
|
|
98
|
+
* breadcrumb can re-anchor the comment onto that ancestor.
|
|
99
|
+
*/
|
|
100
|
+
chain: PickCrumb[];
|
|
101
|
+
/** Highlight rectangle in window coordinates, for the overlay outline. */
|
|
102
|
+
frame: {
|
|
103
|
+
x: number;
|
|
104
|
+
y: number;
|
|
105
|
+
width: number;
|
|
106
|
+
height: number;
|
|
107
|
+
} | null;
|
|
108
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RN WebSocket client for live agent streaming.
|
|
3
|
+
*
|
|
4
|
+
* Connects to the same Metro host the feedback POST uses (derived from
|
|
5
|
+
* `devServerBaseUrl()`), swapping the scheme to `ws(s)` and hitting
|
|
6
|
+
* `/__pinagent/ws` — the endpoint the server mounts via
|
|
7
|
+
* `pinagentWebsocketEndpoints` (Metro `config.server.websocketEndpoints`).
|
|
8
|
+
* Because it rides Metro's own port, a physical device needs no port
|
|
9
|
+
* discovery: if the bundle loaded, this URL is reachable.
|
|
10
|
+
*
|
|
11
|
+
* The wire protocol is the web one (`@pinagent/shared`'s ws-protocol): we send
|
|
12
|
+
* `subscribe` / `user_message` / `ask_response` / `interrupt`, and receive
|
|
13
|
+
* `event` / `done` / `error`. On reconnect the server replays the full
|
|
14
|
+
* transcript, so we fire `onReset` first to let the UI rebuild from scratch.
|
|
15
|
+
*
|
|
16
|
+
* Scoped to ONE feedback id per client — the RN widget streams a single
|
|
17
|
+
* conversation at a time (the one just submitted). That keeps this far smaller
|
|
18
|
+
* than the web client's multiplexed map.
|
|
19
|
+
*/
|
|
20
|
+
import type { AgentEvent } from './transcript';
|
|
21
|
+
export interface StreamHandlers {
|
|
22
|
+
/** A reconnect is about to replay the transcript — clear and rebuild. */
|
|
23
|
+
onReset(): void;
|
|
24
|
+
onEvent(event: AgentEvent): void;
|
|
25
|
+
/** The run's bus closed (agent finished or idle). */
|
|
26
|
+
onDone(): void;
|
|
27
|
+
onError(message: string): void;
|
|
28
|
+
}
|
|
29
|
+
/** Derive `ws(s)://host:port/__pinagent/ws` from Metro's bundle URL. */
|
|
30
|
+
export declare function devServerWsUrl(): string | null;
|
|
31
|
+
export declare class StreamClient {
|
|
32
|
+
private readonly feedbackId;
|
|
33
|
+
private readonly handlers;
|
|
34
|
+
private socket;
|
|
35
|
+
private reconnectDelay;
|
|
36
|
+
private reconnectTimer;
|
|
37
|
+
private closed;
|
|
38
|
+
private connectedBefore;
|
|
39
|
+
constructor(feedbackId: string, handlers: StreamHandlers);
|
|
40
|
+
/** Open the socket and subscribe. Safe to call once per instance. */
|
|
41
|
+
start(): void;
|
|
42
|
+
/** Tear down for good — no further reconnects. Call on unmount/dismiss. */
|
|
43
|
+
stop(): void;
|
|
44
|
+
/** Queue a follow-up turn for the agent. No-op if the socket isn't open. */
|
|
45
|
+
sendUserMessage(content: string): void;
|
|
46
|
+
/** Answer an `ask_user` prompt. */
|
|
47
|
+
sendAskResponse(askId: string, answer: string): void;
|
|
48
|
+
/** Interrupt the in-flight run. */
|
|
49
|
+
interrupt(): void;
|
|
50
|
+
private connect;
|
|
51
|
+
private onMessage;
|
|
52
|
+
private send;
|
|
53
|
+
private scheduleReconnect;
|
|
54
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pinagent/react-native",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
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": [
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"type": "module",
|
|
26
26
|
"exports": {
|
|
27
27
|
".": {
|
|
28
|
+
"types": "./dist/native/index.d.ts",
|
|
28
29
|
"react-native": "./src/native/index.ts",
|
|
29
30
|
"default": "./src/native/index.ts"
|
|
30
31
|
},
|
|
@@ -75,16 +76,17 @@
|
|
|
75
76
|
},
|
|
76
77
|
"devDependencies": {
|
|
77
78
|
"@types/node": "^25.9.1",
|
|
79
|
+
"@types/react": "^19.2.17",
|
|
78
80
|
"@types/ws": "^8.18.1",
|
|
79
81
|
"tsdown": "^0.22.0",
|
|
80
82
|
"typescript": "^6.0.3",
|
|
81
|
-
"@pinagent/
|
|
82
|
-
"@pinagent/
|
|
83
|
+
"@pinagent/db": "0.0.1",
|
|
84
|
+
"@pinagent/agent-runner": "0.0.0"
|
|
83
85
|
},
|
|
84
86
|
"scripts": {
|
|
85
87
|
"prebuild": "node scripts/copy-drizzle.mjs",
|
|
86
|
-
"build": "tsdown",
|
|
88
|
+
"build": "tsdown && tsc -p tsconfig.native.json",
|
|
87
89
|
"dev": "tsdown --watch",
|
|
88
|
-
"typecheck": "tsc --noEmit"
|
|
90
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.native.json --noEmit"
|
|
89
91
|
}
|
|
90
92
|
}
|
package/src/native/inspector.ts
CHANGED
|
@@ -80,8 +80,21 @@ interface RawInspectorData {
|
|
|
80
80
|
closestInstance?: { _debugSource?: RawFiberLike } | null;
|
|
81
81
|
/** Some versions surface the resolved source straight on the payload. */
|
|
82
82
|
source?: RawFiberLike | null;
|
|
83
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* NOT the tapped leaf's props. RN fills this from the nearest authored
|
|
85
|
+
* *composite owner's* FIRST host descendant (`getHostProps` →
|
|
86
|
+
* `findCurrentHostFiber`) — i.e. that component's outermost view — so a tap
|
|
87
|
+
* on a nested child surfaces its container here, not the element under the
|
|
88
|
+
* finger. Used only as a fallback; `closestPublicInstance` pins the leaf.
|
|
89
|
+
*/
|
|
84
90
|
props?: RawProps;
|
|
91
|
+
/**
|
|
92
|
+
* The public instance of the host view actually under the finger — the
|
|
93
|
+
* deepest hit-tested node. Fabric only; Paper passes a numeric view tag we
|
|
94
|
+
* can't bridge, so we degrade to `props`. Its fiber's `memoizedProps` carry
|
|
95
|
+
* the leaf's own `data-pa-loc`. See {@link tappedLeafLoc}.
|
|
96
|
+
*/
|
|
97
|
+
closestPublicInstance?: unknown;
|
|
85
98
|
}
|
|
86
99
|
|
|
87
100
|
let cachedFn: InspectorFn | null | undefined;
|
|
@@ -128,6 +141,8 @@ interface FiberLike {
|
|
|
128
141
|
tag?: number;
|
|
129
142
|
return?: FiberLike | null;
|
|
130
143
|
stateNode?: { canonical?: { publicInstance?: unknown } } | null;
|
|
144
|
+
/** Host fibers carry the committed props — where `data-pa-loc` rides. */
|
|
145
|
+
memoizedProps?: RawProps;
|
|
131
146
|
}
|
|
132
147
|
|
|
133
148
|
let cachedGetHandle: ((instance: unknown) => FiberLike | null) | null | undefined;
|
|
@@ -209,14 +224,57 @@ function paLocOf(props: RawProps): Loc | null {
|
|
|
209
224
|
return parsePaLoc(props?.['data-pa-loc']);
|
|
210
225
|
}
|
|
211
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Walk a host fiber's render-tree parent (`return`) chain, returning the first
|
|
229
|
+
* `data-pa-loc` found on a fiber's `memoizedProps` — the tapped element itself,
|
|
230
|
+
* or, when that exact host is untagged (a 3rd-party / RN-internal view), the
|
|
231
|
+
* nearest authored element enclosing it. Capped against a malformed `return`
|
|
232
|
+
* cycle. Pure over a fiber-like chain; exported for unit testing without an RN
|
|
233
|
+
* runtime.
|
|
234
|
+
*/
|
|
235
|
+
export function nearestPaLocUp(fiber: FiberLike | null): Loc | null {
|
|
236
|
+
for (let i = 0; fiber && i < 10_000; i++) {
|
|
237
|
+
const loc = paLocOf(fiber.memoizedProps);
|
|
238
|
+
if (loc) return loc;
|
|
239
|
+
fiber = fiber.return ?? null;
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The source of the host view actually under the finger.
|
|
246
|
+
*
|
|
247
|
+
* RN's `data.props` is NOT the tapped leaf — it's the nearest authored
|
|
248
|
+
* composite owner's first host descendant (its outer container), so a tap on a
|
|
249
|
+
* nested child resolves to its parent. To land on the leaf, start from the
|
|
250
|
+
* hit-tested host (`data.closestPublicInstance`), bridge it to its fiber, and
|
|
251
|
+
* walk the render-tree parents for the nearest `data-pa-loc`. This mirrors the
|
|
252
|
+
* web widget walking up the DOM from the clicked node.
|
|
253
|
+
*
|
|
254
|
+
* Returns null when the instance can't be bridged (Paper, which surfaces only a
|
|
255
|
+
* numeric view tag) — `pickLoc` then falls back to `data.props`.
|
|
256
|
+
*/
|
|
257
|
+
function tappedLeafLoc(data: RawInspectorData): Loc | null {
|
|
258
|
+
return nearestPaLocUp(getHandleFromPublicInstance(data.closestPublicInstance));
|
|
259
|
+
}
|
|
260
|
+
|
|
212
261
|
/**
|
|
213
262
|
* Resolve the source location. Preferred path: the build-time `data-pa-loc`
|
|
214
|
-
* prop our babel plugin splices on
|
|
215
|
-
*
|
|
216
|
-
*
|
|
263
|
+
* prop our babel plugin splices on — read first from the host view actually
|
|
264
|
+
* under the finger ({@link tappedLeafLoc}), then from `data.props` and each
|
|
265
|
+
* owner outward. Fallback: RN's legacy `_debugSource` / inspector `source`
|
|
266
|
+
* field, for older RN/React where they still exist.
|
|
217
267
|
*/
|
|
218
268
|
function pickLoc(data: RawInspectorData, projectRoot: string): Loc | null {
|
|
219
|
-
//
|
|
269
|
+
// 0. The host view actually under the finger. MUST come first: `data.props`
|
|
270
|
+
// (step 1) is the nearest composite owner's first host — an outer
|
|
271
|
+
// container — so without this a tap on a nested child resolves to its
|
|
272
|
+
// parent (e.g. a card's content tapped, but its screen layout returned).
|
|
273
|
+
const leaf = tappedLeafLoc(data);
|
|
274
|
+
if (leaf) return leaf;
|
|
275
|
+
|
|
276
|
+
// 1. `data-pa-loc` on `data.props` — the nearest authored owner's first host.
|
|
277
|
+
// Fallback for when the touched host can't be bridged to a fiber (Paper).
|
|
220
278
|
const direct = paLocOf(data.props);
|
|
221
279
|
if (direct) return direct;
|
|
222
280
|
|
|
@@ -296,10 +354,12 @@ interface RawCrumb {
|
|
|
296
354
|
*/
|
|
297
355
|
function nearestLoc(locs: (Loc | null)[], i: number): Loc | null {
|
|
298
356
|
for (let j = i + 1; j < locs.length; j++) {
|
|
299
|
-
|
|
357
|
+
const loc = locs[j];
|
|
358
|
+
if (loc) return loc;
|
|
300
359
|
}
|
|
301
360
|
for (let j = i - 1; j >= 0; j--) {
|
|
302
|
-
|
|
361
|
+
const loc = locs[j];
|
|
362
|
+
if (loc) return loc;
|
|
303
363
|
}
|
|
304
364
|
return null;
|
|
305
365
|
}
|
package/src/native/pin-icon.tsx
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* When the peer isn't installed we fall back to a View-drawn teardrop in the
|
|
13
13
|
* same colour, so the FAB always shows a brand pin — never a generic glyph.
|
|
14
14
|
*/
|
|
15
|
-
import type { ReactElement } from 'react';
|
|
15
|
+
import type { ComponentType, ReactElement } from 'react';
|
|
16
16
|
import { View } from 'react-native';
|
|
17
17
|
|
|
18
18
|
// Canonical pinagent pin — mirror of PIN_PATH / BRAND_VIEWBOX in
|
|
@@ -22,9 +22,11 @@ const PIN_PATH =
|
|
|
22
22
|
const VIEWBOX = '0 0 93 93';
|
|
23
23
|
|
|
24
24
|
// The two react-native-svg components we use, or `null` when the peer isn't
|
|
25
|
-
// installed. Typed
|
|
26
|
-
//
|
|
27
|
-
|
|
25
|
+
// installed. Typed as bare component types — `react-native-svg`'s own types
|
|
26
|
+
// aren't a dependency, but `unknown` can't be used as a JSX element, and the
|
|
27
|
+
// native source IS declaration-emitted (see tsconfig.native.json), so these
|
|
28
|
+
// must be valid JSX element types.
|
|
29
|
+
type SvgModule = { Svg: ComponentType<any>; Path: ComponentType<any> } | null;
|
|
28
30
|
|
|
29
31
|
// Resolved once, on first render (dev only — see header). `undefined` = not yet
|
|
30
32
|
// resolved, `null` = peer not installed.
|