@blario/react-native 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +378 -0
- package/dist/BlarProvider.d.ts +74 -0
- package/dist/actions/batched.d.ts +1 -0
- package/dist/actions/click.d.ts +4 -0
- package/dist/actions/commit-watch.d.ts +15 -0
- package/dist/actions/events.d.ts +35 -0
- package/dist/actions/executor.d.ts +29 -0
- package/dist/actions/highlight.d.ts +4 -0
- package/dist/actions/interaction-guards.d.ts +9 -0
- package/dist/actions/landing.d.ts +11 -0
- package/dist/actions/navigation-driver.d.ts +22 -0
- package/dist/actions/parse.d.ts +4 -0
- package/dist/actions/result.d.ts +4 -0
- package/dist/actions/scrolling.d.ts +12 -0
- package/dist/actions/services.d.ts +20 -0
- package/dist/actions/settle.d.ts +13 -0
- package/dist/actions/switch.d.ts +3 -0
- package/dist/actions/target.d.ts +25 -0
- package/dist/actions/text-input.d.ts +9 -0
- package/dist/actions/types.d.ts +59 -0
- package/dist/agent/batch-commands.d.ts +17 -0
- package/dist/agent/compose.d.ts +23 -0
- package/dist/agent/controller.d.ts +28 -0
- package/dist/agent/frame-runner.d.ts +9 -0
- package/dist/agent/frames.d.ts +180 -0
- package/dist/agent/handlers/agent-command.d.ts +5 -0
- package/dist/agent/handlers/batch.d.ts +5 -0
- package/dist/agent/handlers/consent-screenshot.d.ts +4 -0
- package/dist/agent/handlers/context.d.ts +16 -0
- package/dist/agent/handlers/dom-state.d.ts +4 -0
- package/dist/agent/handlers/element-context.d.ts +2 -0
- package/dist/agent/handlers/screenshot.d.ts +3 -0
- package/dist/agent/handlers/text-dom.d.ts +5 -0
- package/dist/agent/stage.d.ts +13 -0
- package/dist/agent-mode/action-description.d.ts +3 -0
- package/dist/agent-mode/bubbles.d.ts +10 -0
- package/dist/agent-mode/colors.d.ts +12 -0
- package/dist/agent-mode/controller.d.ts +92 -0
- package/dist/agent-mode/cursor-follow.d.ts +12 -0
- package/dist/agent-mode/cursor.d.ts +37 -0
- package/dist/agent-mode/frames.d.ts +36 -0
- package/dist/agent-mode/haptics.d.ts +6 -0
- package/dist/agent-mode/keep-awake.d.ts +19 -0
- package/dist/agent-mode/layer-stacking.d.ts +7 -0
- package/dist/agent-mode/markdown.d.ts +19 -0
- package/dist/agent-mode/panel-geometry.d.ts +6 -0
- package/dist/agent-mode/resume.d.ts +13 -0
- package/dist/agent-mode/run-memory.d.ts +19 -0
- package/dist/agent-mode/spotlight-controller.d.ts +68 -0
- package/dist/agent-mode/spotlight.d.ts +6 -0
- package/dist/agent-mode/state.d.ts +9 -0
- package/dist/agent-mode/status.d.ts +11 -0
- package/dist/agent-mode/timers.d.ts +6 -0
- package/dist/agent-mode/ui/AgentDock.d.ts +37 -0
- package/dist/agent-mode/ui/AgentLayer.d.ts +19 -0
- package/dist/agent-mode/ui/AgentLayerHost.d.ts +8 -0
- package/dist/agent-mode/ui/AgentPanel.d.ts +19 -0
- package/dist/agent-mode/ui/AgentTap.d.ts +10 -0
- package/dist/agent-mode/ui/MarkdownText.d.ts +8 -0
- package/dist/agent-mode/ui/MessageBubbles.d.ts +9 -0
- package/dist/agent-mode/ui/PendingOverlay.d.ts +5 -0
- package/dist/agent-mode/ui/SpotlightLayer.d.ts +9 -0
- package/dist/agent-mode/ui/SpotlightOverlay.d.ts +7 -0
- package/dist/agent-mode/ui/StatusCard.d.ts +14 -0
- package/dist/agent-mode/ui/TargetPulse.d.ts +9 -0
- package/dist/agent-mode/ui/TouchShield.d.ts +5 -0
- package/dist/agent-mode/ui/WarningIcon.d.ts +6 -0
- package/dist/agent-mode/ui/full-window-overlay.d.ts +5 -0
- package/dist/agent-mode/ui/layer-host.d.ts +2 -0
- package/dist/backend/api-client.d.ts +38 -0
- package/dist/backend/api-types.d.ts +25 -0
- package/dist/backend/app-info.d.ts +14 -0
- package/dist/backend/backend-transport.d.ts +70 -0
- package/dist/backend/backoff.d.ts +9 -0
- package/dist/backend/base64.d.ts +1 -0
- package/dist/backend/errors.d.ts +17 -0
- package/dist/backend/foreground.d.ts +1 -0
- package/dist/backend/health.d.ts +17 -0
- package/dist/backend/identity-proof.d.ts +23 -0
- package/dist/backend/identity.d.ts +27 -0
- package/dist/backend/message-metadata.d.ts +19 -0
- package/dist/backend/presence.d.ts +20 -0
- package/dist/backend/response-cache.d.ts +9 -0
- package/dist/backend/session-change.d.ts +2 -0
- package/dist/backend/session-socket.d.ts +49 -0
- package/dist/backend/session.d.ts +39 -0
- package/dist/backend/socket-url.d.ts +9 -0
- package/dist/backend/storage.d.ts +13 -0
- package/dist/backend/tab-id.d.ts +2 -0
- package/dist/blar.d.ts +37 -0
- package/dist/chat/ChatLayer.d.ts +29 -0
- package/dist/chat/ChatSheet.d.ts +11 -0
- package/dist/chat/ChatView.d.ts +19 -0
- package/dist/chat/Launcher.d.ts +14 -0
- package/dist/chat/bridge.d.ts +22 -0
- package/dist/chat/chat-coordinator.d.ts +57 -0
- package/dist/chat/chat-state.d.ts +20 -0
- package/dist/chat/navigation.d.ts +5 -0
- package/dist/chat/options.d.ts +22 -0
- package/dist/chat/webview.d.ts +10 -0
- package/dist/chat/widget-bundle.generated.d.ts +2 -0
- package/dist/chat/widget-shell.d.ts +24 -0
- package/dist/confirmation/port.d.ts +12 -0
- package/dist/fiber/locate.d.ts +7 -0
- package/dist/fiber/narrow.d.ts +5 -0
- package/dist/fiber/reader.d.ts +10 -0
- package/dist/fiber/text.d.ts +4 -0
- package/dist/fiber/types.d.ts +21 -0
- package/dist/host/border-radius.d.ts +1 -0
- package/dist/host/instance.d.ts +9 -0
- package/dist/host/measure.d.ts +3 -0
- package/dist/host/rect.d.ts +22 -0
- package/dist/host/visible-area.d.ts +2 -0
- package/dist/index.d.ts +10 -0
- package/dist/nudge/NudgeBubble.d.ts +15 -0
- package/dist/nudge/nudge.d.ts +23 -0
- package/dist/optional/expo-haptics.d.ts +1 -0
- package/dist/optional/expo-keep-awake.d.ts +1 -0
- package/dist/optional/react-native-haptic-feedback.d.ts +1 -0
- package/dist/optional/react-native-keep-awake.d.ts +1 -0
- package/dist/overlay/AgentOverlay.d.ts +5 -0
- package/dist/screenshot/capture-backend.d.ts +6 -0
- package/dist/screenshot/screenshotter.d.ts +18 -0
- package/dist/screenshot/skia.d.ts +35 -0
- package/dist/screenshot/view-shot.d.ts +2 -0
- package/dist/snapshot/capture.d.ts +36 -0
- package/dist/snapshot/classify.d.ts +11 -0
- package/dist/snapshot/collect.d.ts +27 -0
- package/dist/snapshot/element-context.d.ts +9 -0
- package/dist/snapshot/icon.d.ts +2 -0
- package/dist/snapshot/kinds.d.ts +3 -0
- package/dist/snapshot/modals.d.ts +2 -0
- package/dist/snapshot/navigation.d.ts +14 -0
- package/dist/snapshot/ordering.d.ts +2 -0
- package/dist/snapshot/properties.d.ts +14 -0
- package/dist/snapshot/registry.d.ts +16 -0
- package/dist/snapshot/render.d.ts +10 -0
- package/dist/snapshot/route-summary.d.ts +3 -0
- package/dist/snapshot/scroll.d.ts +15 -0
- package/dist/snapshot/text-layout.d.ts +8 -0
- package/dist/snapshot/view.d.ts +30 -0
- package/dist/snapshot/visibility.d.ts +17 -0
- package/dist/snapshot/wrappers.d.ts +11 -0
- package/dist/telemetry/SdkErrorBoundary.d.ts +15 -0
- package/dist/telemetry/build-info.generated.d.ts +2 -0
- package/dist/telemetry/envelope.d.ts +53 -0
- package/dist/telemetry/reporter.d.ts +33 -0
- package/dist/telemetry/scrub.d.ts +1 -0
- package/dist/telemetry/sdk-reporter.d.ts +5 -0
- package/dist/transport/connection-options.d.ts +18 -0
- package/dist/transport/relay-transport.d.ts +17 -0
- package/dist/transport/transport.d.ts +7 -0
- package/dist/ui/insets.d.ts +8 -0
- package/dist/ui/strings.d.ts +33 -0
- package/dist/ui/useInsets.d.ts +2 -0
- package/dist/version.d.ts +1 -0
- package/package.json +84 -3
- package/src/BlarProvider.tsx +462 -0
- package/src/actions/batched.ts +16 -0
- package/src/actions/click.ts +54 -0
- package/src/actions/commit-watch.ts +86 -0
- package/src/actions/events.ts +58 -0
- package/src/actions/executor.ts +147 -0
- package/src/actions/highlight.ts +13 -0
- package/src/actions/interaction-guards.ts +44 -0
- package/src/actions/landing.ts +26 -0
- package/src/actions/navigation-driver.ts +131 -0
- package/src/actions/parse.ts +82 -0
- package/src/actions/result.ts +14 -0
- package/src/actions/scrolling.ts +112 -0
- package/src/actions/services.ts +24 -0
- package/src/actions/settle.ts +40 -0
- package/src/actions/switch.ts +31 -0
- package/src/actions/target.ts +77 -0
- package/src/actions/text-input.ts +82 -0
- package/src/actions/types.ts +52 -0
- package/src/agent/batch-commands.ts +33 -0
- package/src/agent/compose.ts +67 -0
- package/src/agent/controller.ts +106 -0
- package/src/agent/frame-runner.ts +27 -0
- package/src/agent/frames.ts +193 -0
- package/src/agent/handlers/agent-command.ts +74 -0
- package/src/agent/handlers/batch.ts +72 -0
- package/src/agent/handlers/consent-screenshot.ts +44 -0
- package/src/agent/handlers/context.ts +17 -0
- package/src/agent/handlers/dom-state.ts +19 -0
- package/src/agent/handlers/element-context.ts +9 -0
- package/src/agent/handlers/screenshot.ts +24 -0
- package/src/agent/handlers/text-dom.ts +17 -0
- package/src/agent/stage.ts +22 -0
- package/src/agent-mode/action-description.ts +25 -0
- package/src/agent-mode/bubbles.ts +31 -0
- package/src/agent-mode/colors.ts +40 -0
- package/src/agent-mode/controller.ts +459 -0
- package/src/agent-mode/cursor-follow.ts +43 -0
- package/src/agent-mode/cursor.ts +118 -0
- package/src/agent-mode/frames.ts +70 -0
- package/src/agent-mode/haptics.ts +49 -0
- package/src/agent-mode/keep-awake.ts +107 -0
- package/src/agent-mode/layer-stacking.ts +16 -0
- package/src/agent-mode/markdown.ts +84 -0
- package/src/agent-mode/panel-geometry.ts +27 -0
- package/src/agent-mode/resume.ts +16 -0
- package/src/agent-mode/run-memory.ts +65 -0
- package/src/agent-mode/spotlight-controller.ts +255 -0
- package/src/agent-mode/spotlight.ts +14 -0
- package/src/agent-mode/state.ts +36 -0
- package/src/agent-mode/status.ts +13 -0
- package/src/agent-mode/timers.ts +11 -0
- package/src/agent-mode/ui/AgentDock.tsx +285 -0
- package/src/agent-mode/ui/AgentLayer.tsx +123 -0
- package/src/agent-mode/ui/AgentLayerHost.tsx +37 -0
- package/src/agent-mode/ui/AgentPanel.tsx +187 -0
- package/src/agent-mode/ui/AgentTap.tsx +190 -0
- package/src/agent-mode/ui/MarkdownText.tsx +55 -0
- package/src/agent-mode/ui/MessageBubbles.tsx +58 -0
- package/src/agent-mode/ui/PendingOverlay.tsx +32 -0
- package/src/agent-mode/ui/SpotlightLayer.tsx +60 -0
- package/src/agent-mode/ui/SpotlightOverlay.tsx +35 -0
- package/src/agent-mode/ui/StatusCard.tsx +137 -0
- package/src/agent-mode/ui/TargetPulse.tsx +74 -0
- package/src/agent-mode/ui/TouchShield.tsx +22 -0
- package/src/agent-mode/ui/WarningIcon.tsx +32 -0
- package/src/agent-mode/ui/full-window-overlay.ts +16 -0
- package/src/agent-mode/ui/layer-host.ts +11 -0
- package/src/backend/api-client.ts +210 -0
- package/src/backend/api-types.ts +56 -0
- package/src/backend/app-info.ts +40 -0
- package/src/backend/backend-transport.ts +338 -0
- package/src/backend/backoff.ts +16 -0
- package/src/backend/base64.ts +25 -0
- package/src/backend/errors.ts +32 -0
- package/src/backend/foreground.ts +10 -0
- package/src/backend/health.ts +64 -0
- package/src/backend/identity-proof.ts +151 -0
- package/src/backend/identity.ts +31 -0
- package/src/backend/message-metadata.ts +28 -0
- package/src/backend/presence.ts +95 -0
- package/src/backend/response-cache.ts +45 -0
- package/src/backend/session-change.ts +14 -0
- package/src/backend/session-socket.ts +292 -0
- package/src/backend/session.ts +188 -0
- package/src/backend/socket-url.ts +25 -0
- package/src/backend/storage.ts +49 -0
- package/src/backend/tab-id.ts +24 -0
- package/src/blar.ts +100 -0
- package/src/chat/ChatLayer.tsx +109 -0
- package/src/chat/ChatSheet.tsx +82 -0
- package/src/chat/ChatView.tsx +127 -0
- package/src/chat/Launcher.tsx +72 -0
- package/src/chat/bridge.ts +56 -0
- package/src/chat/chat-coordinator.ts +189 -0
- package/src/chat/chat-state.ts +35 -0
- package/src/chat/navigation.ts +15 -0
- package/src/chat/options.ts +39 -0
- package/src/chat/webview.ts +23 -0
- package/src/chat/widget-bundle.generated.ts +2 -0
- package/src/chat/widget-shell.ts +170 -0
- package/src/confirmation/port.ts +15 -0
- package/src/fiber/locate.ts +41 -0
- package/src/fiber/narrow.ts +15 -0
- package/src/fiber/reader.ts +98 -0
- package/src/fiber/text.ts +52 -0
- package/src/fiber/types.ts +24 -0
- package/src/host/border-radius.ts +11 -0
- package/src/host/instance.ts +30 -0
- package/src/host/measure.ts +25 -0
- package/src/host/rect.ts +45 -0
- package/src/host/visible-area.ts +5 -0
- package/src/index.ts +10 -0
- package/src/nudge/NudgeBubble.tsx +117 -0
- package/src/nudge/nudge.ts +100 -0
- package/src/optional/expo-haptics.ts +11 -0
- package/src/optional/expo-keep-awake.ts +11 -0
- package/src/optional/react-native-haptic-feedback.ts +11 -0
- package/src/optional/react-native-keep-awake.ts +11 -0
- package/src/overlay/AgentOverlay.tsx +16 -0
- package/src/screenshot/capture-backend.ts +7 -0
- package/src/screenshot/screenshotter.ts +39 -0
- package/src/screenshot/skia.ts +107 -0
- package/src/screenshot/view-shot.ts +41 -0
- package/src/snapshot/capture.ts +163 -0
- package/src/snapshot/classify.ts +40 -0
- package/src/snapshot/collect.ts +136 -0
- package/src/snapshot/element-context.ts +66 -0
- package/src/snapshot/icon.ts +21 -0
- package/src/snapshot/kinds.ts +10 -0
- package/src/snapshot/modals.ts +21 -0
- package/src/snapshot/navigation.ts +47 -0
- package/src/snapshot/ordering.ts +26 -0
- package/src/snapshot/properties.ts +71 -0
- package/src/snapshot/registry.ts +33 -0
- package/src/snapshot/render.ts +140 -0
- package/src/snapshot/route-summary.ts +15 -0
- package/src/snapshot/scroll.ts +90 -0
- package/src/snapshot/text-layout.ts +59 -0
- package/src/snapshot/view.ts +87 -0
- package/src/snapshot/visibility.ts +63 -0
- package/src/snapshot/wrappers.ts +69 -0
- package/src/telemetry/SdkErrorBoundary.tsx +28 -0
- package/src/telemetry/build-info.generated.ts +2 -0
- package/src/telemetry/envelope.ts +83 -0
- package/src/telemetry/reporter.ts +170 -0
- package/src/telemetry/scrub.ts +10 -0
- package/src/telemetry/sdk-reporter.ts +37 -0
- package/src/transport/connection-options.ts +65 -0
- package/src/transport/relay-transport.ts +82 -0
- package/src/transport/transport.ts +8 -0
- package/src/ui/insets.ts +31 -0
- package/src/ui/strings.ts +102 -0
- package/src/ui/useInsets.ts +10 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface Timers {
|
|
2
|
+
set: (callback: () => void, milliseconds: number) => unknown;
|
|
3
|
+
clear: (handle: unknown) => void;
|
|
4
|
+
now: () => number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const SYSTEM_TIMERS: Timers = {
|
|
8
|
+
set: (callback, milliseconds) => setTimeout(callback, milliseconds),
|
|
9
|
+
clear: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
10
|
+
now: () => Date.now(),
|
|
11
|
+
};
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Animated, Easing, Keyboard, type LayoutRectangle, StyleSheet, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { Band, Rect } from '../../host/rect.ts';
|
|
5
|
+
import type { UiStrings } from '../../ui/strings.ts';
|
|
6
|
+
import { chooseDock, freeBand, messageCap, type PanelDock, STACK_GAP } from '../panel-geometry.ts';
|
|
7
|
+
import type { SpotlightSnapshot } from '../spotlight-controller.ts';
|
|
8
|
+
import { type ActiveSpotlight, AgentPanel, panelChrome } from './AgentPanel.tsx';
|
|
9
|
+
import { StatusCard, type StatusCorners } from './StatusCard.tsx';
|
|
10
|
+
|
|
11
|
+
export interface DockActions {
|
|
12
|
+
place: (id: number, band: Band) => void | Promise<void>;
|
|
13
|
+
approve: () => void;
|
|
14
|
+
cancel: () => void;
|
|
15
|
+
skip: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DockStatus {
|
|
19
|
+
status: string;
|
|
20
|
+
queued: string | null;
|
|
21
|
+
stopLabel: string;
|
|
22
|
+
pulse: number;
|
|
23
|
+
cleared: boolean;
|
|
24
|
+
onStop: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DockLayout {
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
cardWidth: number;
|
|
31
|
+
bottom: number;
|
|
32
|
+
safeTop: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AgentDockProps {
|
|
36
|
+
spotlight: SpotlightSnapshot;
|
|
37
|
+
actions: DockActions;
|
|
38
|
+
card: DockStatus;
|
|
39
|
+
strings: UiStrings;
|
|
40
|
+
color: string;
|
|
41
|
+
reduceMotion: boolean;
|
|
42
|
+
nudge: number;
|
|
43
|
+
layout: DockLayout;
|
|
44
|
+
onCardFrame: (frame: Rect) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const OPEN_MS = 260;
|
|
48
|
+
const CLOSE_MS = 200;
|
|
49
|
+
const REDUCED_MS = 150;
|
|
50
|
+
const STACK_FADE_MS = 150;
|
|
51
|
+
const STACK_SWAP_PAUSE_MS = 60;
|
|
52
|
+
const STACK_RETURN_PAUSE_MS = 140;
|
|
53
|
+
const STACK_RETURN_MS = 200;
|
|
54
|
+
const ENTER_SHIFT = 16;
|
|
55
|
+
const NUDGE_STEP_MS = 80;
|
|
56
|
+
|
|
57
|
+
const OPEN = Easing.bezier(0.22, 1, 0.36, 1);
|
|
58
|
+
const CLOSE = Easing.bezier(0.4, 0, 1, 1);
|
|
59
|
+
|
|
60
|
+
function animate(value: Animated.Value, toValue: number, duration: number, easing: (value: number) => number): Animated.CompositeAnimation {
|
|
61
|
+
return Animated.timing(value, { toValue, duration, easing, useNativeDriver: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function keyboardTop(height: number): number {
|
|
65
|
+
return Keyboard.metrics()?.screenY ?? height;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function AgentDock({ spotlight, actions, card, strings, color, reduceMotion, nudge, layout, onCardFrame }: AgentDockProps): React.JSX.Element {
|
|
69
|
+
const { width, height, cardWidth, bottom, safeTop } = layout;
|
|
70
|
+
const [presented, setPresented] = useState<ActiveSpotlight | null>(null);
|
|
71
|
+
const [dock, setDock] = useState<PanelDock>('bottom');
|
|
72
|
+
const [atTop, setAtTop] = useState(false);
|
|
73
|
+
const [open, setOpen] = useState(false);
|
|
74
|
+
const [cardLayout, setCardLayout] = useState<LayoutRectangle | null>(null);
|
|
75
|
+
const [stackY, setStackY] = useState(0);
|
|
76
|
+
const [panelHeight, setPanelHeight] = useState(0);
|
|
77
|
+
|
|
78
|
+
const panelOpacity = useRef(new Animated.Value(0)).current;
|
|
79
|
+
const panelShift = useRef(new Animated.Value(ENTER_SHIFT)).current;
|
|
80
|
+
const stackOpacity = useRef(new Animated.Value(1)).current;
|
|
81
|
+
const stackShift = useRef(new Animated.Value(0)).current;
|
|
82
|
+
const nudgeShift = useRef(new Animated.Value(0)).current;
|
|
83
|
+
|
|
84
|
+
const presentedId = useRef<number | null>(null);
|
|
85
|
+
const placedId = useRef<number | null>(null);
|
|
86
|
+
const openRef = useRef(false);
|
|
87
|
+
const dockRef = useRef<PanelDock>('bottom');
|
|
88
|
+
const pending = useRef<Array<ReturnType<typeof setTimeout>>>([]);
|
|
89
|
+
|
|
90
|
+
const later = (callback: () => void, milliseconds: number): void => {
|
|
91
|
+
pending.current.push(setTimeout(callback, milliseconds));
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const rest = (): void => {
|
|
95
|
+
pending.current.forEach(clearTimeout);
|
|
96
|
+
pending.current = [];
|
|
97
|
+
[panelOpacity, panelShift, stackOpacity, stackShift].forEach((value) => value.stopAnimation());
|
|
98
|
+
panelOpacity.setValue(0);
|
|
99
|
+
panelShift.setValue(ENTER_SHIFT);
|
|
100
|
+
stackOpacity.setValue(1);
|
|
101
|
+
stackShift.setValue(0);
|
|
102
|
+
openRef.current = false;
|
|
103
|
+
dockRef.current = 'bottom';
|
|
104
|
+
placedId.current = null;
|
|
105
|
+
setOpen(false);
|
|
106
|
+
setDock('bottom');
|
|
107
|
+
setAtTop(false);
|
|
108
|
+
setPanelHeight(0);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const leave = (): void => {
|
|
112
|
+
presentedId.current = null;
|
|
113
|
+
if (!openRef.current) {
|
|
114
|
+
rest();
|
|
115
|
+
setPresented(null);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const duration = reduceMotion ? REDUCED_MS : CLOSE_MS;
|
|
119
|
+
const easing = reduceMotion ? Easing.linear : CLOSE;
|
|
120
|
+
if (dockRef.current === 'bottom') {
|
|
121
|
+
Animated.parallel([animate(panelOpacity, 0, duration, easing), animate(panelShift, reduceMotion ? 0 : ENTER_SHIFT, duration, easing)]).start(() => {
|
|
122
|
+
if (presentedId.current === null) {
|
|
123
|
+
rest();
|
|
124
|
+
setPresented(null);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
Animated.parallel([animate(stackOpacity, 0, duration, easing), animate(stackShift, reduceMotion ? 0 : -ENTER_SHIFT, duration, easing)]).start(() => {
|
|
130
|
+
if (presentedId.current !== null) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
rest();
|
|
134
|
+
stackOpacity.setValue(0);
|
|
135
|
+
setPresented(null);
|
|
136
|
+
later(() => animate(stackOpacity, 1, STACK_RETURN_MS, Easing.out(Easing.ease)).start(), STACK_RETURN_PAUSE_MS);
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (spotlight.kind === 'none') {
|
|
142
|
+
if (presentedId.current !== null) {
|
|
143
|
+
leave();
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (presentedId.current !== spotlight.id) {
|
|
148
|
+
rest();
|
|
149
|
+
presentedId.current = spotlight.id;
|
|
150
|
+
}
|
|
151
|
+
setPresented(spotlight);
|
|
152
|
+
}, [spotlight]);
|
|
153
|
+
|
|
154
|
+
useEffect(() => () => pending.current.forEach(clearTimeout), []);
|
|
155
|
+
|
|
156
|
+
const cardHeight = cardLayout?.height ?? 0;
|
|
157
|
+
const cardBottom = height - bottom;
|
|
158
|
+
const keyboard = keyboardTop(height);
|
|
159
|
+
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (presented === null || presented.shown || placedId.current === presented.id || panelHeight === 0 || cardHeight === 0) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
placedId.current = presented.id;
|
|
165
|
+
const stackTop = cardBottom - cardHeight - panelHeight;
|
|
166
|
+
void actions.place(presented.id, freeBand(safeTop, stackTop, keyboard));
|
|
167
|
+
}, [presented, panelHeight, cardHeight, cardBottom, safeTop, keyboard, actions]);
|
|
168
|
+
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (presented === null || !presented.shown || openRef.current) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
openRef.current = true;
|
|
174
|
+
const left = (width - cardWidth) / 2;
|
|
175
|
+
const stackTop = cardBottom - cardHeight - panelHeight;
|
|
176
|
+
const chosen =
|
|
177
|
+
presented.target === null
|
|
178
|
+
? 'bottom'
|
|
179
|
+
: chooseDock(
|
|
180
|
+
presented.target,
|
|
181
|
+
{ x: left, y: stackTop - STACK_GAP, width: cardWidth, height: cardBottom - stackTop + STACK_GAP },
|
|
182
|
+
{ x: left, y: safeTop + STACK_GAP, width: cardWidth, height: cardHeight + panelHeight + STACK_GAP },
|
|
183
|
+
);
|
|
184
|
+
dockRef.current = chosen;
|
|
185
|
+
setDock(chosen);
|
|
186
|
+
const duration = reduceMotion ? REDUCED_MS : OPEN_MS;
|
|
187
|
+
const easing = reduceMotion ? Easing.linear : OPEN;
|
|
188
|
+
if (chosen === 'bottom') {
|
|
189
|
+
setOpen(true);
|
|
190
|
+
if (reduceMotion) {
|
|
191
|
+
panelShift.setValue(0);
|
|
192
|
+
}
|
|
193
|
+
Animated.parallel([animate(panelOpacity, 1, duration, easing), animate(panelShift, 0, duration, easing)]).start();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
animate(stackOpacity, 0, STACK_FADE_MS, Easing.out(Easing.ease)).start(() => {
|
|
197
|
+
later(() => {
|
|
198
|
+
if (presentedId.current !== presented.id) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
setAtTop(true);
|
|
202
|
+
setOpen(true);
|
|
203
|
+
panelOpacity.setValue(1);
|
|
204
|
+
panelShift.setValue(0);
|
|
205
|
+
stackShift.setValue(reduceMotion ? 0 : -ENTER_SHIFT);
|
|
206
|
+
Animated.parallel([animate(stackOpacity, 1, duration, easing), animate(stackShift, 0, duration, easing)]).start();
|
|
207
|
+
}, STACK_SWAP_PAUSE_MS);
|
|
208
|
+
});
|
|
209
|
+
}, [presented]);
|
|
210
|
+
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (nudge === 0 || reduceMotion) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const step = (toValue: number): Animated.CompositeAnimation => animate(nudgeShift, toValue, NUDGE_STEP_MS, Easing.out(Easing.ease));
|
|
216
|
+
Animated.sequence([step(-6), step(0), step(-3), step(0)]).start();
|
|
217
|
+
}, [nudge, reduceMotion, nudgeShift]);
|
|
218
|
+
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
if (cardLayout !== null) {
|
|
221
|
+
onCardFrame({ x: (width - cardWidth) / 2, y: stackY + cardLayout.y, width: cardWidth, height: cardLayout.height });
|
|
222
|
+
}
|
|
223
|
+
}, [cardLayout, stackY, width, cardWidth, onCardFrame]);
|
|
224
|
+
|
|
225
|
+
const confirming = presented?.kind === 'confirming';
|
|
226
|
+
const corners: StatusCorners = !open ? 'round' : atTop ? 'square-bottom' : 'square-top';
|
|
227
|
+
const room = Math.min(cardBottom - cardHeight - (presented === null ? 0 : panelChrome(presented)), keyboard) - safeTop - STACK_GAP * 2;
|
|
228
|
+
|
|
229
|
+
const statusCard = (
|
|
230
|
+
<View onLayout={(event) => setCardLayout(event.nativeEvent.layout)}>
|
|
231
|
+
<StatusCard
|
|
232
|
+
status={confirming ? strings.confirmationTitle : card.status}
|
|
233
|
+
queued={card.queued}
|
|
234
|
+
stopLabel={card.stopLabel}
|
|
235
|
+
color={color}
|
|
236
|
+
stopHidden={confirming}
|
|
237
|
+
corners={corners}
|
|
238
|
+
pulse={card.pulse}
|
|
239
|
+
cleared={card.cleared}
|
|
240
|
+
onStop={card.onStop}
|
|
241
|
+
/>
|
|
242
|
+
</View>
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const panel =
|
|
246
|
+
presented === null ? null : (
|
|
247
|
+
<Animated.View
|
|
248
|
+
pointerEvents={open ? 'auto' : 'none'}
|
|
249
|
+
onLayout={(event) => setPanelHeight(event.nativeEvent.layout.height)}
|
|
250
|
+
style={{ opacity: panelOpacity, transform: [{ translateY: panelShift }] }}
|
|
251
|
+
>
|
|
252
|
+
<AgentPanel
|
|
253
|
+
spotlight={presented}
|
|
254
|
+
dock={dock}
|
|
255
|
+
strings={strings}
|
|
256
|
+
color={color}
|
|
257
|
+
messageMaxHeight={messageCap(room)}
|
|
258
|
+
onConfirm={() => actions.approve()}
|
|
259
|
+
onStop={() => actions.cancel()}
|
|
260
|
+
onSkip={() => actions.skip()}
|
|
261
|
+
/>
|
|
262
|
+
</Animated.View>
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<Animated.View
|
|
267
|
+
pointerEvents="box-none"
|
|
268
|
+
onLayout={(event) => setStackY(event.nativeEvent.layout.y)}
|
|
269
|
+
style={[
|
|
270
|
+
styles.stack,
|
|
271
|
+
{ left: (width - cardWidth) / 2, width: cardWidth },
|
|
272
|
+
atTop ? { top: safeTop + STACK_GAP } : { bottom },
|
|
273
|
+
{ opacity: stackOpacity, transform: [{ translateY: Animated.add(stackShift, nudgeShift) }] },
|
|
274
|
+
]}
|
|
275
|
+
>
|
|
276
|
+
{atTop ? statusCard : null}
|
|
277
|
+
{panel}
|
|
278
|
+
{atTop ? null : statusCard}
|
|
279
|
+
</Animated.View>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const styles = StyleSheet.create({
|
|
284
|
+
stack: { position: 'absolute' },
|
|
285
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { StyleSheet, Text, useWindowDimensions, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { intersects, type Rect } from '../../host/rect.ts';
|
|
5
|
+
import type { UiStrings } from '../../ui/strings.ts';
|
|
6
|
+
import { useInsets } from '../../ui/useInsets.ts';
|
|
7
|
+
import type { AgentModeSnapshot } from '../controller.ts';
|
|
8
|
+
import type { CursorSnapshot } from '../cursor.ts';
|
|
9
|
+
import type { SpotlightSnapshot } from '../spotlight-controller.ts';
|
|
10
|
+
import type { AgentStatus } from '../status.ts';
|
|
11
|
+
import { AgentDock, type DockActions } from './AgentDock.tsx';
|
|
12
|
+
import { AgentTap } from './AgentTap.tsx';
|
|
13
|
+
import { MessageBubbles } from './MessageBubbles.tsx';
|
|
14
|
+
import { PendingOverlay } from './PendingOverlay.tsx';
|
|
15
|
+
import { SpotlightLayer } from './SpotlightLayer.tsx';
|
|
16
|
+
import { TouchShield } from './TouchShield.tsx';
|
|
17
|
+
|
|
18
|
+
export interface AgentLayerProps {
|
|
19
|
+
snapshot: AgentModeSnapshot;
|
|
20
|
+
strings: UiStrings;
|
|
21
|
+
color: string;
|
|
22
|
+
spotlight: SpotlightSnapshot;
|
|
23
|
+
spotlightActions: DockActions;
|
|
24
|
+
cursor: CursorSnapshot;
|
|
25
|
+
reduceMotion: boolean;
|
|
26
|
+
onCursorAttach: () => void;
|
|
27
|
+
onCursorDetach: () => void;
|
|
28
|
+
onStop: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const CARD_MAX_WIDTH = 380;
|
|
32
|
+
const CARD_MARGIN = 16;
|
|
33
|
+
const CARD_BOTTOM = 20;
|
|
34
|
+
const BUBBLES_BOTTOM = 96;
|
|
35
|
+
const TOAST_BOTTOM = 100;
|
|
36
|
+
const TOAST_MS = 2_000;
|
|
37
|
+
|
|
38
|
+
function statusText(status: AgentStatus, strings: UiStrings): string {
|
|
39
|
+
switch (status.kind) {
|
|
40
|
+
case 'thinking':
|
|
41
|
+
return strings.agentThinking;
|
|
42
|
+
case 'executing':
|
|
43
|
+
return strings.agentExecuting;
|
|
44
|
+
case 'text':
|
|
45
|
+
return status.text;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function useBlockedTap(): { toast: boolean; pulse: number; block: () => void } {
|
|
50
|
+
const [toast, setToast] = useState(false);
|
|
51
|
+
const [pulse, setPulse] = useState(0);
|
|
52
|
+
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
53
|
+
useEffect(
|
|
54
|
+
() => () => {
|
|
55
|
+
if (timer.current !== null) {
|
|
56
|
+
clearTimeout(timer.current);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
[],
|
|
60
|
+
);
|
|
61
|
+
const block = (): void => {
|
|
62
|
+
setToast(true);
|
|
63
|
+
setPulse((count) => count + 1);
|
|
64
|
+
if (timer.current !== null) {
|
|
65
|
+
clearTimeout(timer.current);
|
|
66
|
+
}
|
|
67
|
+
timer.current = setTimeout(() => setToast(false), TOAST_MS);
|
|
68
|
+
};
|
|
69
|
+
return { toast, pulse, block };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function AgentLayer({ snapshot, strings, color, spotlight, spotlightActions, cursor, reduceMotion, onCursorAttach, onCursorDetach, onStop }: AgentLayerProps): React.JSX.Element {
|
|
73
|
+
const insets = useInsets();
|
|
74
|
+
const { width, height } = useWindowDimensions();
|
|
75
|
+
const { toast, pulse, block } = useBlockedTap();
|
|
76
|
+
const [cardFrame, setCardFrame] = useState<Rect | null>(null);
|
|
77
|
+
const [nudge, setNudge] = useState(0);
|
|
78
|
+
if (snapshot.phase === 'pending') {
|
|
79
|
+
return <PendingOverlay text={strings.agentTakingControl} />;
|
|
80
|
+
}
|
|
81
|
+
const cardWidth = Math.min(CARD_MAX_WIDTH, width - CARD_MARGIN * 2);
|
|
82
|
+
const tapUnderCard = cursor.target !== null && cardFrame !== null && intersects(cursor.target, cardFrame);
|
|
83
|
+
const queued = snapshot.queued === null ? null : (snapshot.queued.message ?? strings.agentQueued);
|
|
84
|
+
const answering = spotlight.kind === 'confirming' && spotlight.shown;
|
|
85
|
+
return (
|
|
86
|
+
<View style={StyleSheet.absoluteFill} pointerEvents="box-none">
|
|
87
|
+
{answering ? null : <TouchShield onBlockedTap={block} />}
|
|
88
|
+
{spotlight.kind === 'none' ? (
|
|
89
|
+
<MessageBubbles bubbles={snapshot.bubbles} width={cardWidth} bottom={BUBBLES_BOTTOM + insets.bottom} linkColor={color} />
|
|
90
|
+
) : null}
|
|
91
|
+
{toast ? (
|
|
92
|
+
<View pointerEvents="none" style={[styles.toast, { bottom: TOAST_BOTTOM + insets.bottom }]}>
|
|
93
|
+
<Text style={styles.toastText}>{strings.agentTapStopToControl}</Text>
|
|
94
|
+
</View>
|
|
95
|
+
) : null}
|
|
96
|
+
<SpotlightLayer spotlight={spotlight} color={color} reduceMotion={reduceMotion} onBackdropPress={() => setNudge((count) => count + 1)} />
|
|
97
|
+
<AgentDock
|
|
98
|
+
spotlight={spotlight}
|
|
99
|
+
actions={spotlightActions}
|
|
100
|
+
card={{ status: statusText(snapshot.status, strings), queued, stopLabel: strings.agentStop, pulse, cleared: tapUnderCard, onStop }}
|
|
101
|
+
strings={strings}
|
|
102
|
+
color={color}
|
|
103
|
+
reduceMotion={reduceMotion}
|
|
104
|
+
nudge={nudge}
|
|
105
|
+
layout={{ width, height, cardWidth, bottom: CARD_BOTTOM + insets.bottom, safeTop: insets.top }}
|
|
106
|
+
onCardFrame={setCardFrame}
|
|
107
|
+
/>
|
|
108
|
+
<AgentTap snapshot={cursor} color={color} reduceMotion={reduceMotion} onAttach={onCursorAttach} onDetach={onCursorDetach} />
|
|
109
|
+
</View>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const styles = StyleSheet.create({
|
|
114
|
+
toast: {
|
|
115
|
+
position: 'absolute',
|
|
116
|
+
alignSelf: 'center',
|
|
117
|
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
118
|
+
borderRadius: 8,
|
|
119
|
+
paddingHorizontal: 20,
|
|
120
|
+
paddingVertical: 10,
|
|
121
|
+
},
|
|
122
|
+
toastText: { color: '#ffffff', fontSize: 14 },
|
|
123
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React, { type ReactNode } from 'react';
|
|
2
|
+
import { Modal, Platform, StyleSheet, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { loadFullWindowOverlay } from './full-window-overlay.ts';
|
|
5
|
+
import { chooseLayerHost } from './layer-host.ts';
|
|
6
|
+
|
|
7
|
+
export interface AgentLayerHostProps {
|
|
8
|
+
visible: boolean;
|
|
9
|
+
generation: number;
|
|
10
|
+
onRequestClose: () => void;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const FULL_WINDOW_OVERLAY = Platform.OS === 'ios' ? loadFullWindowOverlay() : null;
|
|
15
|
+
const HOST = chooseLayerHost(Platform.OS, FULL_WINDOW_OVERLAY !== null);
|
|
16
|
+
|
|
17
|
+
export function AgentLayerHost({ visible, generation, onRequestClose, children }: AgentLayerHostProps): React.JSX.Element | null {
|
|
18
|
+
if (!visible) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
if (HOST === 'modal') {
|
|
22
|
+
return (
|
|
23
|
+
<Modal key={generation} visible transparent animationType="none" hardwareAccelerated onRequestClose={onRequestClose}>
|
|
24
|
+
{children}
|
|
25
|
+
</Modal>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
const content = (
|
|
29
|
+
<View style={StyleSheet.absoluteFill} pointerEvents="box-none">
|
|
30
|
+
{children}
|
|
31
|
+
</View>
|
|
32
|
+
);
|
|
33
|
+
if (HOST === 'full-window-overlay' && FULL_WINDOW_OVERLAY !== null) {
|
|
34
|
+
return <FULL_WINDOW_OVERLAY>{content}</FULL_WINDOW_OVERLAY>;
|
|
35
|
+
}
|
|
36
|
+
return content;
|
|
37
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { AccessibilityInfo, Animated, Easing, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { UiStrings } from '../../ui/strings.ts';
|
|
5
|
+
import { describeAction } from '../action-description.ts';
|
|
6
|
+
import { luminance, onColor } from '../colors.ts';
|
|
7
|
+
import { plainText } from '../markdown.ts';
|
|
8
|
+
import type { PanelDock } from '../panel-geometry.ts';
|
|
9
|
+
import type { SpotlightSnapshot } from '../spotlight-controller.ts';
|
|
10
|
+
import { MarkdownText } from './MarkdownText.tsx';
|
|
11
|
+
import { WarningIcon } from './WarningIcon.tsx';
|
|
12
|
+
|
|
13
|
+
export type ActiveSpotlight = Exclude<SpotlightSnapshot, { kind: 'none' }>;
|
|
14
|
+
|
|
15
|
+
export interface AgentPanelProps {
|
|
16
|
+
spotlight: ActiveSpotlight;
|
|
17
|
+
dock: PanelDock;
|
|
18
|
+
strings: UiStrings;
|
|
19
|
+
color: string;
|
|
20
|
+
messageMaxHeight: number;
|
|
21
|
+
onConfirm: () => void;
|
|
22
|
+
onStop: () => void;
|
|
23
|
+
onSkip: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DESTRUCTIVE = '#dc2626';
|
|
27
|
+
const WARNING_TEXT = '#92400e';
|
|
28
|
+
const WARNING_BACKGROUND = '#fef3c7';
|
|
29
|
+
const INK = '#1f2937';
|
|
30
|
+
|
|
31
|
+
const PADDING = 18;
|
|
32
|
+
const BUTTON_HEIGHT = 48;
|
|
33
|
+
const SKIP_HEIGHT = 44;
|
|
34
|
+
const WARNING_HEIGHT = 42;
|
|
35
|
+
const CONFIRM_GAP = 16;
|
|
36
|
+
const DESTRUCTIVE_GAP = 12;
|
|
37
|
+
const READING_GAP = 10;
|
|
38
|
+
const READING_BOTTOM = 6;
|
|
39
|
+
|
|
40
|
+
export function panelChrome(spotlight: ActiveSpotlight): number {
|
|
41
|
+
if (spotlight.kind === 'reading') {
|
|
42
|
+
return PADDING + READING_GAP + SKIP_HEIGHT + READING_BOTTOM;
|
|
43
|
+
}
|
|
44
|
+
if (spotlight.request.riskLevel === 'destructive') {
|
|
45
|
+
return PADDING + DESTRUCTIVE_GAP + WARNING_HEIGHT + DESTRUCTIVE_GAP + 4 + BUTTON_HEIGHT + PADDING;
|
|
46
|
+
}
|
|
47
|
+
return PADDING + CONFIRM_GAP + BUTTON_HEIGHT + PADDING;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function messageOf(spotlight: ActiveSpotlight, strings: UiStrings): string {
|
|
51
|
+
if (spotlight.kind === 'reading') {
|
|
52
|
+
return spotlight.text ?? strings.actionFallback;
|
|
53
|
+
}
|
|
54
|
+
return spotlight.request.explanation?.trim() || describeAction(spotlight.request.action, strings);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function barColor(color: string): string {
|
|
58
|
+
return 1.05 / ((luminance(color) ?? 0) + 0.05) < 1.5 ? INK : color;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function Progress({ durationMs, running, color }: { durationMs: number; running: boolean; color: string }): React.JSX.Element {
|
|
62
|
+
const progress = useRef(new Animated.Value(0)).current;
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (running) {
|
|
65
|
+
Animated.timing(progress, { toValue: 1, duration: durationMs, easing: Easing.linear, useNativeDriver: true }).start();
|
|
66
|
+
}
|
|
67
|
+
}, [running, durationMs, progress]);
|
|
68
|
+
return (
|
|
69
|
+
<View style={styles.track}>
|
|
70
|
+
<Animated.View style={[styles.bar, { backgroundColor: barColor(color), transform: [{ scaleX: progress }] }]} />
|
|
71
|
+
</View>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function AgentPanel({ spotlight, dock, strings, color, messageMaxHeight, onConfirm, onStop, onSkip }: AgentPanelProps): React.JSX.Element {
|
|
76
|
+
const message = messageOf(spotlight, strings);
|
|
77
|
+
const confirming = spotlight.kind === 'confirming';
|
|
78
|
+
const destructive = confirming && spotlight.request.riskLevel === 'destructive';
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (confirming && spotlight.shown) {
|
|
82
|
+
AccessibilityInfo.announceForAccessibility(plainText(message));
|
|
83
|
+
}
|
|
84
|
+
}, [confirming, spotlight.shown, message]);
|
|
85
|
+
|
|
86
|
+
const body = (
|
|
87
|
+
<ScrollView style={{ maxHeight: messageMaxHeight }} bounces={false} nestedScrollEnabled>
|
|
88
|
+
<View style={spotlight.kind === 'reading' ? styles.readingMessage : null}>
|
|
89
|
+
<MarkdownText source={message} linkColor={color} textStyle={styles.message} />
|
|
90
|
+
</View>
|
|
91
|
+
</ScrollView>
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<View
|
|
96
|
+
accessibilityViewIsModal={confirming}
|
|
97
|
+
style={[
|
|
98
|
+
styles.panel,
|
|
99
|
+
dock === 'bottom' ? styles.bottom : styles.top,
|
|
100
|
+
confirming ? (destructive ? styles.destructive : styles.confirm) : styles.reading,
|
|
101
|
+
]}
|
|
102
|
+
>
|
|
103
|
+
{body}
|
|
104
|
+
{spotlight.kind === 'reading' ? (
|
|
105
|
+
<View style={styles.progressRow}>
|
|
106
|
+
<Progress durationMs={spotlight.durationMs} running={spotlight.shown} color={color} />
|
|
107
|
+
<Pressable accessibilityRole="button" onPress={onSkip} style={styles.skip}>
|
|
108
|
+
<Text style={styles.skipText}>{strings.readingSkip}</Text>
|
|
109
|
+
</Pressable>
|
|
110
|
+
</View>
|
|
111
|
+
) : (
|
|
112
|
+
<>
|
|
113
|
+
{destructive ? (
|
|
114
|
+
<View style={styles.warning}>
|
|
115
|
+
<WarningIcon color={WARNING_TEXT} background={WARNING_BACKGROUND} />
|
|
116
|
+
<Text style={styles.warningText}>{strings.confirmCannotBeUndone}</Text>
|
|
117
|
+
</View>
|
|
118
|
+
) : null}
|
|
119
|
+
<View style={[styles.buttons, destructive ? styles.destructiveButtons : null]}>
|
|
120
|
+
<Pressable accessibilityRole="button" onPress={onStop} style={[styles.button, styles.stop]}>
|
|
121
|
+
<Text style={[styles.buttonText, { color: INK }]}>{strings.confirmStop}</Text>
|
|
122
|
+
</Pressable>
|
|
123
|
+
<Pressable
|
|
124
|
+
accessibilityRole="button"
|
|
125
|
+
onPress={onConfirm}
|
|
126
|
+
style={[styles.button, destructive ? { backgroundColor: DESTRUCTIVE } : [styles.confirmButton, { backgroundColor: color }]]}
|
|
127
|
+
>
|
|
128
|
+
<Text style={[styles.buttonText, { color: destructive ? '#ffffff' : onColor(color) }]}>
|
|
129
|
+
{destructive ? strings.confirmProceedDestructive : strings.confirmProceed}
|
|
130
|
+
</Text>
|
|
131
|
+
</Pressable>
|
|
132
|
+
</View>
|
|
133
|
+
</>
|
|
134
|
+
)}
|
|
135
|
+
</View>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const styles = StyleSheet.create({
|
|
140
|
+
panel: {
|
|
141
|
+
backgroundColor: '#ffffff',
|
|
142
|
+
shadowColor: '#000000',
|
|
143
|
+
elevation: 12,
|
|
144
|
+
},
|
|
145
|
+
bottom: {
|
|
146
|
+
borderTopLeftRadius: 16,
|
|
147
|
+
borderTopRightRadius: 16,
|
|
148
|
+
shadowOffset: { width: 0, height: -6 },
|
|
149
|
+
shadowOpacity: 0.14,
|
|
150
|
+
shadowRadius: 9,
|
|
151
|
+
},
|
|
152
|
+
top: {
|
|
153
|
+
borderBottomLeftRadius: 16,
|
|
154
|
+
borderBottomRightRadius: 16,
|
|
155
|
+
shadowOffset: { width: 0, height: 8 },
|
|
156
|
+
shadowOpacity: 0.18,
|
|
157
|
+
shadowRadius: 10,
|
|
158
|
+
},
|
|
159
|
+
confirm: { padding: PADDING, gap: CONFIRM_GAP },
|
|
160
|
+
destructive: { padding: PADDING, gap: DESTRUCTIVE_GAP },
|
|
161
|
+
reading: { paddingTop: PADDING, paddingRight: 12, paddingBottom: READING_BOTTOM, paddingLeft: PADDING, gap: READING_GAP },
|
|
162
|
+
message: { fontSize: 16, lineHeight: 23, color: INK },
|
|
163
|
+
readingMessage: { paddingRight: 6 },
|
|
164
|
+
warning: {
|
|
165
|
+
flexDirection: 'row',
|
|
166
|
+
alignItems: 'center',
|
|
167
|
+
gap: 8,
|
|
168
|
+
minHeight: WARNING_HEIGHT,
|
|
169
|
+
paddingHorizontal: 12,
|
|
170
|
+
borderWidth: 1,
|
|
171
|
+
borderColor: '#f59e0b',
|
|
172
|
+
borderRadius: 10,
|
|
173
|
+
backgroundColor: WARNING_BACKGROUND,
|
|
174
|
+
},
|
|
175
|
+
warningText: { flex: 1, fontSize: 14, color: WARNING_TEXT },
|
|
176
|
+
buttons: { flexDirection: 'row', gap: 10 },
|
|
177
|
+
destructiveButtons: { marginTop: 4 },
|
|
178
|
+
button: { flex: 1, height: BUTTON_HEIGHT, borderRadius: 12, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 12 },
|
|
179
|
+
stop: { backgroundColor: '#eef0f3' },
|
|
180
|
+
confirmButton: { borderWidth: 1, borderColor: 'rgba(0, 0, 0, 0.1)' },
|
|
181
|
+
buttonText: { fontSize: 16, fontWeight: '600' },
|
|
182
|
+
progressRow: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
|
183
|
+
track: { flex: 1, height: 4, borderRadius: 2, overflow: 'hidden', backgroundColor: 'rgba(0, 0, 0, 0.08)' },
|
|
184
|
+
bar: { height: '100%', width: '100%', transformOrigin: 'left' },
|
|
185
|
+
skip: { height: SKIP_HEIGHT, paddingHorizontal: 12, borderRadius: 10, alignItems: 'center', justifyContent: 'center' },
|
|
186
|
+
skipText: { fontSize: 15, fontWeight: '600', color: '#4b5563' },
|
|
187
|
+
});
|