@tonyclaw/agent-inspector 3.0.24 → 3.0.25
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/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-BC52FYwn.js → CompareDrawer-BnvOPwpe.js} +1 -1
- package/.output/public/assets/ProxyViewerContainer-DcqFqo1m.js +127 -0
- package/.output/public/assets/{ReplayDialog-BcKsarKt.js → ReplayDialog-BJgoQWdW.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-DR0uGpCj.js → RequestAnatomy-CpBZEHnM.js} +1 -1
- package/.output/public/assets/{ResponseView-kxAAPfmn.js → ResponseView-CWmyRb2M.js} +2 -2
- package/.output/public/assets/{StreamingChunkSequence-YQR0PjQj.js → StreamingChunkSequence-BWhC8bwx.js} +1 -1
- package/.output/public/assets/{_sessionId-DRl-Y9om.js → _sessionId-Cq-2OqUU.js} +1 -1
- package/.output/public/assets/{index-CQbrc-hi.js → index-BKi9IRtV.js} +3 -3
- package/.output/public/assets/index-BMYuDL5X.css +1 -0
- package/.output/public/assets/{index-CcSIBI4L.js → index-Cf8O0Oos.js} +1 -1
- package/.output/public/assets/{index-ZrMgyEZV.js → index-DdgTJ4QJ.js} +1 -1
- package/.output/public/assets/{json-viewer-CS5pwnEA.js → json-viewer-Bqu_4hmm.js} +1 -1
- package/.output/server/_libs/lucide-react.mjs +245 -217
- package/.output/server/{_sessionId-BLk2xPkR.mjs → _sessionId-Ba7H3Gjw.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-Cw7Dseo3.mjs → CompareDrawer-aH59Ab6x.mjs} +3 -3
- package/.output/server/_ssr/{ProxyViewerContainer-D8SB0zyQ.mjs → ProxyViewerContainer-By-I0tJv.mjs} +539 -254
- package/.output/server/_ssr/{ReplayDialog-B-YMhZkE.mjs → ReplayDialog-BO4Vy0et.mjs} +4 -4
- package/.output/server/_ssr/{RequestAnatomy-a9d8UTYZ.mjs → RequestAnatomy-FtOr7vSS.mjs} +3 -3
- package/.output/server/_ssr/{ResponseView-PzaZS4CN.mjs → ResponseView-cvw6ajue.mjs} +3 -3
- package/.output/server/_ssr/{StreamingChunkSequence-C21_TCXk.mjs → StreamingChunkSequence-Oc8S2kD6.mjs} +3 -3
- package/.output/server/_ssr/{index-CrCeNP7H.mjs → index-BDRlQheR.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-Di2gxs9H.mjs → json-viewer-Cd6trhQr.mjs} +3 -3
- package/.output/server/_ssr/{router-BJzdUbcH.mjs → router-bofP-Ekl.mjs} +4 -4
- package/.output/server/{_tanstack-start-manifest_v-CGBTdAYC.mjs → _tanstack-start-manifest_v-BPuf86qB.mjs} +1 -1
- package/.output/server/index.mjs +70 -70
- package/package.json +1 -1
- package/src/components/ProxyViewer.tsx +2 -280
- package/src/components/inspector-pet/InspectorPet.tsx +390 -0
- package/src/components/inspector-pet/inspectorPetLogic.ts +273 -0
- package/styles/globals.css +98 -1
- package/.output/public/assets/ProxyViewerContainer-WuChOrSS.js +0 -127
- package/.output/public/assets/index-DQeXi4Rv.css +0 -1
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type ChangeEvent,
|
|
3
|
-
type CSSProperties,
|
|
4
3
|
type JSX,
|
|
5
|
-
type PointerEvent as ReactPointerEvent,
|
|
6
4
|
type ReactNode,
|
|
7
5
|
useCallback,
|
|
8
6
|
useEffect,
|
|
@@ -81,6 +79,7 @@ import { AlertsDialog } from "./alerts/AlertsDialog";
|
|
|
81
79
|
import { GroupsDialog } from "./groups/GroupsDialog";
|
|
82
80
|
import { SettingsDialog } from "./providers/SettingsDialog";
|
|
83
81
|
import { AgentLabDialog } from "./ecosystem/AgentLabDialog";
|
|
82
|
+
import { InspectorPet } from "./inspector-pet/InspectorPet";
|
|
84
83
|
import { PiAgentPanel } from "./pi-agent/PiAgentPanel";
|
|
85
84
|
import { computeCacheTrends } from "./proxy-viewer/cacheTrend";
|
|
86
85
|
import { LazyCompareDrawer } from "./proxy-viewer/lazy";
|
|
@@ -1056,25 +1055,6 @@ function BrandHeader({
|
|
|
1056
1055
|
);
|
|
1057
1056
|
}
|
|
1058
1057
|
|
|
1059
|
-
type FloatingLogoPosition = {
|
|
1060
|
-
x: number;
|
|
1061
|
-
y: number;
|
|
1062
|
-
};
|
|
1063
|
-
|
|
1064
|
-
type FloatingLogoDragState = {
|
|
1065
|
-
pointerId: number;
|
|
1066
|
-
offsetX: number;
|
|
1067
|
-
offsetY: number;
|
|
1068
|
-
};
|
|
1069
|
-
|
|
1070
|
-
type FloatingLogoEdge = "left" | "right" | "top" | "bottom";
|
|
1071
|
-
|
|
1072
|
-
const FLOATING_LOGO_SIZE = 56;
|
|
1073
|
-
const FLOATING_LOGO_MARGIN = 14;
|
|
1074
|
-
const FLOATING_LOGO_MIN_DRIFT_MS = 5200;
|
|
1075
|
-
const FLOATING_LOGO_DRIFT_WINDOW_MS = 2400;
|
|
1076
|
-
const FLOATING_LOGO_MIN_STEP = 72;
|
|
1077
|
-
const FLOATING_LOGO_MAX_STEP = 156;
|
|
1078
1058
|
const DETAILS_PANEL_COLLAPSED_STORAGE_KEY = "agent-inspector.detailsPanelCollapsed";
|
|
1079
1059
|
|
|
1080
1060
|
function readStoredDetailsPanelCollapsed(): boolean {
|
|
@@ -1083,264 +1063,6 @@ function readStoredDetailsPanelCollapsed(): boolean {
|
|
|
1083
1063
|
return stored === null || stored === "1";
|
|
1084
1064
|
}
|
|
1085
1065
|
|
|
1086
|
-
function clampFloatingLogoPosition(position: FloatingLogoPosition): FloatingLogoPosition {
|
|
1087
|
-
const maxX = Math.max(
|
|
1088
|
-
FLOATING_LOGO_MARGIN,
|
|
1089
|
-
window.innerWidth - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
|
|
1090
|
-
);
|
|
1091
|
-
const maxY = Math.max(
|
|
1092
|
-
FLOATING_LOGO_MARGIN,
|
|
1093
|
-
window.innerHeight - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
|
|
1094
|
-
);
|
|
1095
|
-
return {
|
|
1096
|
-
x: Math.min(Math.max(position.x, FLOATING_LOGO_MARGIN), maxX),
|
|
1097
|
-
y: Math.min(Math.max(position.y, FLOATING_LOGO_MARGIN), maxY),
|
|
1098
|
-
};
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
function snapFloatingLogoToEdge(position: FloatingLogoPosition): FloatingLogoPosition {
|
|
1102
|
-
const clamped = clampFloatingLogoPosition(position);
|
|
1103
|
-
const maxX = Math.max(
|
|
1104
|
-
FLOATING_LOGO_MARGIN,
|
|
1105
|
-
window.innerWidth - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
|
|
1106
|
-
);
|
|
1107
|
-
const maxY = Math.max(
|
|
1108
|
-
FLOATING_LOGO_MARGIN,
|
|
1109
|
-
window.innerHeight - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
|
|
1110
|
-
);
|
|
1111
|
-
const leftDistance = clamped.x - FLOATING_LOGO_MARGIN;
|
|
1112
|
-
const rightDistance = maxX - clamped.x;
|
|
1113
|
-
const topDistance = clamped.y - FLOATING_LOGO_MARGIN;
|
|
1114
|
-
const bottomDistance = maxY - clamped.y;
|
|
1115
|
-
const nearestDistance = Math.min(leftDistance, rightDistance, topDistance, bottomDistance);
|
|
1116
|
-
|
|
1117
|
-
if (nearestDistance === leftDistance) return { x: FLOATING_LOGO_MARGIN, y: clamped.y };
|
|
1118
|
-
if (nearestDistance === rightDistance) return { x: maxX, y: clamped.y };
|
|
1119
|
-
if (nearestDistance === topDistance) return { x: clamped.x, y: FLOATING_LOGO_MARGIN };
|
|
1120
|
-
return { x: clamped.x, y: maxY };
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
function randomFloatingLogoEdgePosition(): FloatingLogoPosition {
|
|
1124
|
-
const maxX = Math.max(
|
|
1125
|
-
FLOATING_LOGO_MARGIN,
|
|
1126
|
-
window.innerWidth - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
|
|
1127
|
-
);
|
|
1128
|
-
const maxY = Math.max(
|
|
1129
|
-
FLOATING_LOGO_MARGIN,
|
|
1130
|
-
window.innerHeight - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
|
|
1131
|
-
);
|
|
1132
|
-
const randomX = FLOATING_LOGO_MARGIN + Math.random() * (maxX - FLOATING_LOGO_MARGIN);
|
|
1133
|
-
const randomY = FLOATING_LOGO_MARGIN + Math.random() * (maxY - FLOATING_LOGO_MARGIN);
|
|
1134
|
-
const edgeIndex = Math.floor(Math.random() * 4);
|
|
1135
|
-
|
|
1136
|
-
switch (edgeIndex) {
|
|
1137
|
-
case 0:
|
|
1138
|
-
return { x: FLOATING_LOGO_MARGIN, y: randomY };
|
|
1139
|
-
case 1:
|
|
1140
|
-
return { x: maxX, y: randomY };
|
|
1141
|
-
case 2:
|
|
1142
|
-
return { x: randomX, y: FLOATING_LOGO_MARGIN };
|
|
1143
|
-
case 3:
|
|
1144
|
-
return { x: randomX, y: maxY };
|
|
1145
|
-
default:
|
|
1146
|
-
return { x: maxX, y: FLOATING_LOGO_MARGIN };
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
function nearestFloatingLogoEdge(position: FloatingLogoPosition): FloatingLogoEdge {
|
|
1151
|
-
const clamped = clampFloatingLogoPosition(position);
|
|
1152
|
-
const maxX = Math.max(
|
|
1153
|
-
FLOATING_LOGO_MARGIN,
|
|
1154
|
-
window.innerWidth - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
|
|
1155
|
-
);
|
|
1156
|
-
const maxY = Math.max(
|
|
1157
|
-
FLOATING_LOGO_MARGIN,
|
|
1158
|
-
window.innerHeight - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
|
|
1159
|
-
);
|
|
1160
|
-
const distances: { edge: FloatingLogoEdge; distance: number }[] = [
|
|
1161
|
-
{ edge: "left", distance: clamped.x - FLOATING_LOGO_MARGIN },
|
|
1162
|
-
{ edge: "right", distance: maxX - clamped.x },
|
|
1163
|
-
{ edge: "top", distance: clamped.y - FLOATING_LOGO_MARGIN },
|
|
1164
|
-
{ edge: "bottom", distance: maxY - clamped.y },
|
|
1165
|
-
];
|
|
1166
|
-
return distances.reduce((best, current) => (current.distance < best.distance ? current : best))
|
|
1167
|
-
.edge;
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
function nextFloatingLogoWalkingPosition(position: FloatingLogoPosition): FloatingLogoPosition {
|
|
1171
|
-
const clamped = snapFloatingLogoToEdge(position);
|
|
1172
|
-
const maxX = Math.max(
|
|
1173
|
-
FLOATING_LOGO_MARGIN,
|
|
1174
|
-
window.innerWidth - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
|
|
1175
|
-
);
|
|
1176
|
-
const maxY = Math.max(
|
|
1177
|
-
FLOATING_LOGO_MARGIN,
|
|
1178
|
-
window.innerHeight - FLOATING_LOGO_SIZE - FLOATING_LOGO_MARGIN,
|
|
1179
|
-
);
|
|
1180
|
-
const step =
|
|
1181
|
-
FLOATING_LOGO_MIN_STEP + Math.random() * (FLOATING_LOGO_MAX_STEP - FLOATING_LOGO_MIN_STEP);
|
|
1182
|
-
const direction = Math.random() > 0.5 ? 1 : -1;
|
|
1183
|
-
const edge = nearestFloatingLogoEdge(clamped);
|
|
1184
|
-
|
|
1185
|
-
switch (edge) {
|
|
1186
|
-
case "left": {
|
|
1187
|
-
const nextY = clamped.y + step * direction;
|
|
1188
|
-
if (nextY <= FLOATING_LOGO_MARGIN)
|
|
1189
|
-
return { x: FLOATING_LOGO_MARGIN, y: FLOATING_LOGO_MARGIN };
|
|
1190
|
-
if (nextY >= maxY) return { x: FLOATING_LOGO_MARGIN, y: maxY };
|
|
1191
|
-
return { x: FLOATING_LOGO_MARGIN, y: nextY };
|
|
1192
|
-
}
|
|
1193
|
-
case "right": {
|
|
1194
|
-
const nextY = clamped.y + step * direction;
|
|
1195
|
-
if (nextY <= FLOATING_LOGO_MARGIN) return { x: maxX, y: FLOATING_LOGO_MARGIN };
|
|
1196
|
-
if (nextY >= maxY) return { x: maxX, y: maxY };
|
|
1197
|
-
return { x: maxX, y: nextY };
|
|
1198
|
-
}
|
|
1199
|
-
case "top": {
|
|
1200
|
-
const nextX = clamped.x + step * direction;
|
|
1201
|
-
if (nextX <= FLOATING_LOGO_MARGIN)
|
|
1202
|
-
return { x: FLOATING_LOGO_MARGIN, y: FLOATING_LOGO_MARGIN };
|
|
1203
|
-
if (nextX >= maxX) return { x: maxX, y: FLOATING_LOGO_MARGIN };
|
|
1204
|
-
return { x: nextX, y: FLOATING_LOGO_MARGIN };
|
|
1205
|
-
}
|
|
1206
|
-
case "bottom": {
|
|
1207
|
-
const nextX = clamped.x + step * direction;
|
|
1208
|
-
if (nextX <= FLOATING_LOGO_MARGIN) return { x: FLOATING_LOGO_MARGIN, y: maxY };
|
|
1209
|
-
if (nextX >= maxX) return { x: maxX, y: maxY };
|
|
1210
|
-
return { x: nextX, y: maxY };
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
function FloatingInspectorLogo(): JSX.Element {
|
|
1216
|
-
const [position, setPosition] = useState<FloatingLogoPosition>({
|
|
1217
|
-
x: FLOATING_LOGO_MARGIN,
|
|
1218
|
-
y: FLOATING_LOGO_MARGIN,
|
|
1219
|
-
});
|
|
1220
|
-
const [motionTick, setMotionTick] = useState(0);
|
|
1221
|
-
const [dragging, setDragging] = useState(false);
|
|
1222
|
-
const dragStateRef = useRef<FloatingLogoDragState | null>(null);
|
|
1223
|
-
|
|
1224
|
-
useEffect(() => {
|
|
1225
|
-
setPosition(randomFloatingLogoEdgePosition());
|
|
1226
|
-
}, []);
|
|
1227
|
-
|
|
1228
|
-
useEffect(() => {
|
|
1229
|
-
if (dragging) return undefined;
|
|
1230
|
-
let timeoutId: number | null = null;
|
|
1231
|
-
|
|
1232
|
-
const scheduleDrift = () => {
|
|
1233
|
-
const delay = FLOATING_LOGO_MIN_DRIFT_MS + Math.random() * FLOATING_LOGO_DRIFT_WINDOW_MS;
|
|
1234
|
-
timeoutId = window.setTimeout(() => {
|
|
1235
|
-
setMotionTick((current) => current + 1);
|
|
1236
|
-
setPosition((current) => nextFloatingLogoWalkingPosition(current));
|
|
1237
|
-
scheduleDrift();
|
|
1238
|
-
}, delay);
|
|
1239
|
-
};
|
|
1240
|
-
|
|
1241
|
-
scheduleDrift();
|
|
1242
|
-
return () => {
|
|
1243
|
-
if (timeoutId !== null) {
|
|
1244
|
-
window.clearTimeout(timeoutId);
|
|
1245
|
-
}
|
|
1246
|
-
};
|
|
1247
|
-
}, [dragging]);
|
|
1248
|
-
|
|
1249
|
-
useEffect(() => {
|
|
1250
|
-
const handleResize = () => {
|
|
1251
|
-
setPosition((current) => snapFloatingLogoToEdge(current));
|
|
1252
|
-
};
|
|
1253
|
-
window.addEventListener("resize", handleResize);
|
|
1254
|
-
return () => window.removeEventListener("resize", handleResize);
|
|
1255
|
-
}, []);
|
|
1256
|
-
|
|
1257
|
-
const handlePointerDown = useCallback((event: ReactPointerEvent<HTMLButtonElement>) => {
|
|
1258
|
-
const rect = event.currentTarget.getBoundingClientRect();
|
|
1259
|
-
dragStateRef.current = {
|
|
1260
|
-
pointerId: event.pointerId,
|
|
1261
|
-
offsetX: event.clientX - rect.left,
|
|
1262
|
-
offsetY: event.clientY - rect.top,
|
|
1263
|
-
};
|
|
1264
|
-
event.currentTarget.setPointerCapture(event.pointerId);
|
|
1265
|
-
setDragging(true);
|
|
1266
|
-
}, []);
|
|
1267
|
-
|
|
1268
|
-
const handlePointerMove = useCallback((event: ReactPointerEvent<HTMLButtonElement>) => {
|
|
1269
|
-
const dragState = dragStateRef.current;
|
|
1270
|
-
if (dragState === null || dragState.pointerId !== event.pointerId) return;
|
|
1271
|
-
setPosition(
|
|
1272
|
-
clampFloatingLogoPosition({
|
|
1273
|
-
x: event.clientX - dragState.offsetX,
|
|
1274
|
-
y: event.clientY - dragState.offsetY,
|
|
1275
|
-
}),
|
|
1276
|
-
);
|
|
1277
|
-
}, []);
|
|
1278
|
-
|
|
1279
|
-
const handlePointerUp = useCallback((event: ReactPointerEvent<HTMLButtonElement>) => {
|
|
1280
|
-
const dragState = dragStateRef.current;
|
|
1281
|
-
if (dragState === null || dragState.pointerId !== event.pointerId) return;
|
|
1282
|
-
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
1283
|
-
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
1284
|
-
}
|
|
1285
|
-
dragStateRef.current = null;
|
|
1286
|
-
setDragging(false);
|
|
1287
|
-
setPosition((current) => snapFloatingLogoToEdge(current));
|
|
1288
|
-
}, []);
|
|
1289
|
-
|
|
1290
|
-
const style = useMemo<CSSProperties>(
|
|
1291
|
-
() => ({
|
|
1292
|
-
transform: `translate3d(${position.x}px, ${position.y}px, 0) rotate(${
|
|
1293
|
-
motionTick % 2 === 0 ? "-4deg" : "5deg"
|
|
1294
|
-
}) scale(${dragging ? "1.08" : "1"})`,
|
|
1295
|
-
transition: dragging ? "none" : "transform 4800ms cubic-bezier(0.33, 1, 0.68, 1)",
|
|
1296
|
-
touchAction: "none",
|
|
1297
|
-
}),
|
|
1298
|
-
[dragging, motionTick, position.x, position.y],
|
|
1299
|
-
);
|
|
1300
|
-
|
|
1301
|
-
return (
|
|
1302
|
-
<button
|
|
1303
|
-
type="button"
|
|
1304
|
-
className={cn(
|
|
1305
|
-
"fixed left-0 top-0 z-40 flex size-14 items-center justify-center rounded-full bg-transparent text-foreground outline-none transition-[filter,opacity] duration-200 hover:opacity-95 hover:drop-shadow-[0_0_24px_rgba(245,158,11,0.36)] focus-visible:ring-1 focus-visible:ring-amber-300/55",
|
|
1306
|
-
dragging ? "cursor-grabbing" : "cursor-grab",
|
|
1307
|
-
)}
|
|
1308
|
-
style={style}
|
|
1309
|
-
onPointerDown={handlePointerDown}
|
|
1310
|
-
onPointerMove={handlePointerMove}
|
|
1311
|
-
onPointerUp={handlePointerUp}
|
|
1312
|
-
onPointerCancel={handlePointerUp}
|
|
1313
|
-
aria-label="Floating Inspector logo"
|
|
1314
|
-
title="Drag Inspector logo"
|
|
1315
|
-
>
|
|
1316
|
-
<span
|
|
1317
|
-
className={cn(
|
|
1318
|
-
"relative flex size-12 items-center justify-center",
|
|
1319
|
-
!dragging && "floating-crab-body",
|
|
1320
|
-
)}
|
|
1321
|
-
aria-hidden="true"
|
|
1322
|
-
>
|
|
1323
|
-
<CrabLogo className="relative z-10 size-11 text-amber-500 drop-shadow-[0_0_15px_rgba(245,158,11,0.30)]" />
|
|
1324
|
-
<span className="pointer-events-none absolute inset-x-0 bottom-0 z-0 flex items-end justify-center gap-1 text-amber-400/80">
|
|
1325
|
-
{[0, 1, 2, 3, 4, 5].map((leg) => (
|
|
1326
|
-
<span
|
|
1327
|
-
key={leg}
|
|
1328
|
-
className={cn(
|
|
1329
|
-
"floating-crab-leg h-2 w-0.5 rounded-full bg-current shadow-[0_0_7px_rgba(245,158,11,0.24)]",
|
|
1330
|
-
leg < 3 ? "floating-crab-leg-left" : "floating-crab-leg-right",
|
|
1331
|
-
dragging && "animate-none",
|
|
1332
|
-
)}
|
|
1333
|
-
style={{
|
|
1334
|
-
animationDelay: `${leg % 2 === 0 ? 0 : 180}ms`,
|
|
1335
|
-
}}
|
|
1336
|
-
/>
|
|
1337
|
-
))}
|
|
1338
|
-
</span>
|
|
1339
|
-
</span>
|
|
1340
|
-
</button>
|
|
1341
|
-
);
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
1066
|
function SessionContextLogButton({ label, logId }: { label: string; logId: number }): JSX.Element {
|
|
1345
1067
|
return (
|
|
1346
1068
|
<button
|
|
@@ -2019,7 +1741,7 @@ export function ProxyViewer({
|
|
|
2019
1741
|
<PanelRightClose className="size-3.5" />
|
|
2020
1742
|
)}
|
|
2021
1743
|
</button>
|
|
2022
|
-
{detailsPanelCollapsed && <
|
|
1744
|
+
{detailsPanelCollapsed && <InspectorPet logs={logs} />}
|
|
2023
1745
|
<div
|
|
2024
1746
|
className={cn(
|
|
2025
1747
|
"grid w-full min-w-0 grid-cols-1 lg:min-h-0 lg:flex-1 lg:overflow-hidden",
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CSSProperties,
|
|
3
|
+
type JSX,
|
|
4
|
+
type PointerEvent as ReactPointerEvent,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { Heart, Pause, Play } from "lucide-react";
|
|
12
|
+
import type { CapturedLog } from "../../contracts";
|
|
13
|
+
import { cn } from "../../lib/utils";
|
|
14
|
+
import { CrabLogo } from "../ui/crab-logo";
|
|
15
|
+
import {
|
|
16
|
+
INSPECTOR_PET_MARGIN,
|
|
17
|
+
type InspectorPetMood,
|
|
18
|
+
type InspectorPetPosition,
|
|
19
|
+
type InspectorPetViewport,
|
|
20
|
+
clampInspectorPetPosition,
|
|
21
|
+
inspectorPetPanelPosition,
|
|
22
|
+
nextInspectorPetWalkingPosition,
|
|
23
|
+
randomInspectorPetEdgePosition,
|
|
24
|
+
resolveInspectorPetStatus,
|
|
25
|
+
snapInspectorPetToEdge,
|
|
26
|
+
} from "./inspectorPetLogic";
|
|
27
|
+
|
|
28
|
+
type InspectorPetProps = {
|
|
29
|
+
logs: readonly CapturedLog[];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type InspectorPetDragState = {
|
|
33
|
+
pointerId: number;
|
|
34
|
+
offsetX: number;
|
|
35
|
+
offsetY: number;
|
|
36
|
+
startX: number;
|
|
37
|
+
startY: number;
|
|
38
|
+
moved: boolean;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const INSPECTOR_PET_ROAMING_STORAGE_KEY = "agent-inspector.pet.roaming";
|
|
42
|
+
const INSPECTOR_PET_MIN_DRIFT_MS = 5200;
|
|
43
|
+
const INSPECTOR_PET_DRIFT_WINDOW_MS = 2400;
|
|
44
|
+
const INSPECTOR_PET_PANEL_DISMISS_MS = 6000;
|
|
45
|
+
const INSPECTOR_PET_REACTION_MS = 760;
|
|
46
|
+
const INSPECTOR_PET_DRAG_THRESHOLD = 4;
|
|
47
|
+
|
|
48
|
+
function readViewport(): InspectorPetViewport {
|
|
49
|
+
if (typeof window === "undefined") return { width: 1280, height: 720, scale: 1 };
|
|
50
|
+
const rawScale = window
|
|
51
|
+
.getComputedStyle(document.documentElement)
|
|
52
|
+
.getPropertyValue("--inspector-ui-scale");
|
|
53
|
+
const parsedScale = Number.parseFloat(rawScale);
|
|
54
|
+
const scale = Number.isFinite(parsedScale) && parsedScale > 0 ? parsedScale : 1;
|
|
55
|
+
return {
|
|
56
|
+
width: Math.max(window.innerWidth, INSPECTOR_PET_MARGIN * 2),
|
|
57
|
+
height: Math.max(window.innerHeight, INSPECTOR_PET_MARGIN * 2),
|
|
58
|
+
scale,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readStoredRoaming(): boolean {
|
|
63
|
+
if (typeof window === "undefined") return true;
|
|
64
|
+
try {
|
|
65
|
+
return window.localStorage.getItem(INSPECTOR_PET_ROAMING_STORAGE_KEY) !== "0";
|
|
66
|
+
} catch {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function moodDotClass(mood: InspectorPetMood): string {
|
|
72
|
+
switch (mood) {
|
|
73
|
+
case "active":
|
|
74
|
+
return "bg-cyan-400 shadow-[0_0_9px_rgba(34,211,238,0.65)]";
|
|
75
|
+
case "attention":
|
|
76
|
+
return "bg-red-500 shadow-[0_0_9px_rgba(239,68,68,0.55)]";
|
|
77
|
+
case "idle":
|
|
78
|
+
return "bg-amber-400 shadow-[0_0_8px_rgba(251,191,36,0.45)]";
|
|
79
|
+
case "sleeping":
|
|
80
|
+
return "bg-muted-foreground/45";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function moodMotionClass(mood: InspectorPetMood): string {
|
|
85
|
+
switch (mood) {
|
|
86
|
+
case "active":
|
|
87
|
+
return "inspector-pet-active";
|
|
88
|
+
case "attention":
|
|
89
|
+
return "inspector-pet-attention";
|
|
90
|
+
case "idle":
|
|
91
|
+
return "";
|
|
92
|
+
case "sleeping":
|
|
93
|
+
return "inspector-pet-sleeping";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function InspectorPet({ logs }: InspectorPetProps): JSX.Element {
|
|
98
|
+
const [viewport, setViewport] = useState<InspectorPetViewport>(readViewport);
|
|
99
|
+
const [position, setPosition] = useState<InspectorPetPosition>({
|
|
100
|
+
x: INSPECTOR_PET_MARGIN,
|
|
101
|
+
y: INSPECTOR_PET_MARGIN,
|
|
102
|
+
});
|
|
103
|
+
const [clockMs, setClockMs] = useState(() => Date.now());
|
|
104
|
+
const [motionTick, setMotionTick] = useState(0);
|
|
105
|
+
const [panelTick, setPanelTick] = useState(0);
|
|
106
|
+
const [panelVisible, setPanelVisible] = useState(false);
|
|
107
|
+
const [reactionTick, setReactionTick] = useState(0);
|
|
108
|
+
const [reactionVisible, setReactionVisible] = useState(false);
|
|
109
|
+
const [dragging, setDragging] = useState(false);
|
|
110
|
+
const [roaming, setRoaming] = useState(readStoredRoaming);
|
|
111
|
+
const [reduceMotion, setReduceMotion] = useState(false);
|
|
112
|
+
const dragStateRef = useRef<InspectorPetDragState | null>(null);
|
|
113
|
+
const dragOccurredRef = useRef(false);
|
|
114
|
+
|
|
115
|
+
const status = useMemo(() => resolveInspectorPetStatus(logs, clockMs), [clockMs, logs]);
|
|
116
|
+
const panelPosition = useMemo(
|
|
117
|
+
() => inspectorPetPanelPosition(position, viewport),
|
|
118
|
+
[position, viewport],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
const nextViewport = readViewport();
|
|
123
|
+
setViewport(nextViewport);
|
|
124
|
+
setPosition(randomInspectorPetEdgePosition(nextViewport, Math.random(), Math.random()));
|
|
125
|
+
}, []);
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
129
|
+
const handleChange = (event: MediaQueryListEvent): void => setReduceMotion(event.matches);
|
|
130
|
+
setReduceMotion(mediaQuery.matches);
|
|
131
|
+
mediaQuery.addEventListener("change", handleChange);
|
|
132
|
+
return () => mediaQuery.removeEventListener("change", handleChange);
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
const intervalId = window.setInterval(() => setClockMs(Date.now()), 15_000);
|
|
137
|
+
return () => window.clearInterval(intervalId);
|
|
138
|
+
}, []);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
try {
|
|
142
|
+
window.localStorage.setItem(INSPECTOR_PET_ROAMING_STORAGE_KEY, roaming ? "1" : "0");
|
|
143
|
+
} catch {
|
|
144
|
+
// Local storage can be unavailable in hardened browser profiles.
|
|
145
|
+
}
|
|
146
|
+
}, [roaming]);
|
|
147
|
+
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (!roaming || dragging || reduceMotion) return undefined;
|
|
150
|
+
let timeoutId: number | null = null;
|
|
151
|
+
|
|
152
|
+
const scheduleDrift = (): void => {
|
|
153
|
+
const delay = INSPECTOR_PET_MIN_DRIFT_MS + Math.random() * INSPECTOR_PET_DRIFT_WINDOW_MS;
|
|
154
|
+
timeoutId = window.setTimeout(() => {
|
|
155
|
+
setMotionTick((current) => current + 1);
|
|
156
|
+
setPosition((current) =>
|
|
157
|
+
nextInspectorPetWalkingPosition(current, viewport, Math.random(), Math.random()),
|
|
158
|
+
);
|
|
159
|
+
scheduleDrift();
|
|
160
|
+
}, delay);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
scheduleDrift();
|
|
164
|
+
return () => {
|
|
165
|
+
if (timeoutId !== null) window.clearTimeout(timeoutId);
|
|
166
|
+
};
|
|
167
|
+
}, [dragging, reduceMotion, roaming, viewport]);
|
|
168
|
+
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
const handleResize = (): void => {
|
|
171
|
+
const nextViewport = readViewport();
|
|
172
|
+
setViewport(nextViewport);
|
|
173
|
+
setPosition((current) => snapInspectorPetToEdge(current, nextViewport));
|
|
174
|
+
};
|
|
175
|
+
window.addEventListener("resize", handleResize);
|
|
176
|
+
return () => window.removeEventListener("resize", handleResize);
|
|
177
|
+
}, []);
|
|
178
|
+
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (!panelVisible) return undefined;
|
|
181
|
+
const timeoutId = window.setTimeout(
|
|
182
|
+
() => setPanelVisible(false),
|
|
183
|
+
INSPECTOR_PET_PANEL_DISMISS_MS,
|
|
184
|
+
);
|
|
185
|
+
return () => window.clearTimeout(timeoutId);
|
|
186
|
+
}, [panelTick, panelVisible]);
|
|
187
|
+
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
if (!reactionVisible) return undefined;
|
|
190
|
+
const timeoutId = window.setTimeout(() => setReactionVisible(false), INSPECTOR_PET_REACTION_MS);
|
|
191
|
+
return () => window.clearTimeout(timeoutId);
|
|
192
|
+
}, [reactionTick, reactionVisible]);
|
|
193
|
+
|
|
194
|
+
const handlePet = useCallback(() => {
|
|
195
|
+
if (dragOccurredRef.current) {
|
|
196
|
+
dragOccurredRef.current = false;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
setClockMs(Date.now());
|
|
200
|
+
setPanelTick((current) => current + 1);
|
|
201
|
+
setPanelVisible(true);
|
|
202
|
+
setReactionTick((current) => current + 1);
|
|
203
|
+
setReactionVisible(true);
|
|
204
|
+
}, []);
|
|
205
|
+
|
|
206
|
+
const handlePointerDown = useCallback((event: ReactPointerEvent<HTMLButtonElement>) => {
|
|
207
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
208
|
+
dragOccurredRef.current = false;
|
|
209
|
+
dragStateRef.current = {
|
|
210
|
+
pointerId: event.pointerId,
|
|
211
|
+
offsetX: event.clientX - rect.left,
|
|
212
|
+
offsetY: event.clientY - rect.top,
|
|
213
|
+
startX: event.clientX,
|
|
214
|
+
startY: event.clientY,
|
|
215
|
+
moved: false,
|
|
216
|
+
};
|
|
217
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
218
|
+
setPanelVisible(false);
|
|
219
|
+
setDragging(true);
|
|
220
|
+
}, []);
|
|
221
|
+
|
|
222
|
+
const handlePointerMove = useCallback(
|
|
223
|
+
(event: ReactPointerEvent<HTMLButtonElement>) => {
|
|
224
|
+
const dragState = dragStateRef.current;
|
|
225
|
+
if (dragState === null || dragState.pointerId !== event.pointerId) return;
|
|
226
|
+
const moved =
|
|
227
|
+
dragState.moved ||
|
|
228
|
+
Math.abs(event.clientX - dragState.startX) >= INSPECTOR_PET_DRAG_THRESHOLD ||
|
|
229
|
+
Math.abs(event.clientY - dragState.startY) >= INSPECTOR_PET_DRAG_THRESHOLD;
|
|
230
|
+
dragState.moved = moved;
|
|
231
|
+
dragOccurredRef.current = moved;
|
|
232
|
+
setPosition(
|
|
233
|
+
clampInspectorPetPosition(
|
|
234
|
+
{
|
|
235
|
+
x: event.clientX - dragState.offsetX,
|
|
236
|
+
y: event.clientY - dragState.offsetY,
|
|
237
|
+
},
|
|
238
|
+
viewport,
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
},
|
|
242
|
+
[viewport],
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const handlePointerUp = useCallback(
|
|
246
|
+
(event: ReactPointerEvent<HTMLButtonElement>) => {
|
|
247
|
+
const dragState = dragStateRef.current;
|
|
248
|
+
if (dragState === null || dragState.pointerId !== event.pointerId) return;
|
|
249
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
250
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
251
|
+
}
|
|
252
|
+
dragOccurredRef.current = dragState.moved;
|
|
253
|
+
dragStateRef.current = null;
|
|
254
|
+
setDragging(false);
|
|
255
|
+
setPosition((current) => snapInspectorPetToEdge(current, viewport));
|
|
256
|
+
},
|
|
257
|
+
[viewport],
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const handlePointerCancel = useCallback((event: ReactPointerEvent<HTMLButtonElement>) => {
|
|
261
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
262
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
263
|
+
}
|
|
264
|
+
dragStateRef.current = null;
|
|
265
|
+
dragOccurredRef.current = false;
|
|
266
|
+
setDragging(false);
|
|
267
|
+
}, []);
|
|
268
|
+
|
|
269
|
+
const petStyle = useMemo<CSSProperties>(
|
|
270
|
+
() => ({
|
|
271
|
+
transform: `translate3d(${String(position.x)}px, ${String(position.y)}px, 0) rotate(${
|
|
272
|
+
motionTick % 2 === 0 ? "-4deg" : "5deg"
|
|
273
|
+
}) scale(${dragging ? "1.08" : "1"})`,
|
|
274
|
+
transition: dragging ? "none" : "transform 4800ms cubic-bezier(0.33, 1, 0.68, 1)",
|
|
275
|
+
touchAction: "none",
|
|
276
|
+
}),
|
|
277
|
+
[dragging, motionTick, position.x, position.y],
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const panelStyle = useMemo<CSSProperties>(
|
|
281
|
+
() => ({ left: panelPosition.x, top: panelPosition.y }),
|
|
282
|
+
[panelPosition.x, panelPosition.y],
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const roamingLabel = roaming ? "Pause pet roaming" : "Resume pet roaming";
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<>
|
|
289
|
+
<button
|
|
290
|
+
type="button"
|
|
291
|
+
className={cn(
|
|
292
|
+
"fixed left-0 top-0 z-30 flex size-14 items-center justify-center bg-transparent text-foreground outline-none transition-[filter,opacity] duration-200 hover:opacity-95 hover:drop-shadow-[0_0_24px_rgba(245,158,11,0.36)] focus-visible:drop-shadow-[0_0_12px_rgba(251,191,36,0.72)]",
|
|
293
|
+
dragging ? "cursor-grabbing" : "cursor-grab",
|
|
294
|
+
)}
|
|
295
|
+
style={petStyle}
|
|
296
|
+
data-pet-mood={status.mood}
|
|
297
|
+
onClick={handlePet}
|
|
298
|
+
onPointerDown={handlePointerDown}
|
|
299
|
+
onPointerMove={handlePointerMove}
|
|
300
|
+
onPointerUp={handlePointerUp}
|
|
301
|
+
onPointerCancel={handlePointerCancel}
|
|
302
|
+
aria-label={`Inspector pet: ${status.label}`}
|
|
303
|
+
title={status.label}
|
|
304
|
+
>
|
|
305
|
+
<span
|
|
306
|
+
className={cn(
|
|
307
|
+
"relative flex size-12 items-center justify-center",
|
|
308
|
+
reactionVisible && "inspector-pet-patted",
|
|
309
|
+
)}
|
|
310
|
+
aria-hidden="true"
|
|
311
|
+
>
|
|
312
|
+
<span
|
|
313
|
+
className={cn(
|
|
314
|
+
"relative flex size-12 items-center justify-center",
|
|
315
|
+
moodMotionClass(status.mood),
|
|
316
|
+
)}
|
|
317
|
+
>
|
|
318
|
+
<span
|
|
319
|
+
className={cn(
|
|
320
|
+
"relative flex size-12 items-center justify-center",
|
|
321
|
+
!dragging && "floating-crab-body",
|
|
322
|
+
)}
|
|
323
|
+
>
|
|
324
|
+
<CrabLogo className="relative z-10 size-11 text-amber-500 drop-shadow-[0_0_15px_rgba(245,158,11,0.30)]" />
|
|
325
|
+
<span className="pointer-events-none absolute inset-x-0 bottom-0 z-0 flex items-end justify-center gap-1 text-amber-400/80">
|
|
326
|
+
{[0, 1, 2, 3, 4, 5].map((leg) => (
|
|
327
|
+
<span
|
|
328
|
+
key={leg}
|
|
329
|
+
className={cn(
|
|
330
|
+
"floating-crab-leg h-2 w-0.5 rounded-full bg-current shadow-[0_0_7px_rgba(245,158,11,0.24)]",
|
|
331
|
+
leg < 3 ? "floating-crab-leg-left" : "floating-crab-leg-right",
|
|
332
|
+
dragging && "animate-none",
|
|
333
|
+
)}
|
|
334
|
+
style={{ animationDelay: `${String(leg % 2 === 0 ? 0 : 180)}ms` }}
|
|
335
|
+
/>
|
|
336
|
+
))}
|
|
337
|
+
</span>
|
|
338
|
+
</span>
|
|
339
|
+
</span>
|
|
340
|
+
<span
|
|
341
|
+
className={cn(
|
|
342
|
+
"absolute right-0 top-1 size-2 rounded-full border border-background/90",
|
|
343
|
+
moodDotClass(status.mood),
|
|
344
|
+
status.mood === "active" && !reduceMotion && "animate-pulse",
|
|
345
|
+
)}
|
|
346
|
+
/>
|
|
347
|
+
{reactionVisible && (
|
|
348
|
+
<Heart
|
|
349
|
+
key={reactionTick}
|
|
350
|
+
className="inspector-pet-heart absolute -right-1 -top-2 size-3.5 fill-amber-400 text-amber-400"
|
|
351
|
+
/>
|
|
352
|
+
)}
|
|
353
|
+
</span>
|
|
354
|
+
</button>
|
|
355
|
+
|
|
356
|
+
{panelVisible && (
|
|
357
|
+
<div
|
|
358
|
+
className="fixed z-50 w-48 rounded-md border border-border/80 bg-popover/95 px-3 py-2 text-popover-foreground shadow-[0_14px_34px_rgba(0,0,0,0.28)] backdrop-blur"
|
|
359
|
+
style={panelStyle}
|
|
360
|
+
role="status"
|
|
361
|
+
aria-live="polite"
|
|
362
|
+
>
|
|
363
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
364
|
+
<span className={cn("size-2 shrink-0 rounded-full", moodDotClass(status.mood))} />
|
|
365
|
+
<span className="min-w-0 flex-1 truncate text-xs font-medium">{status.label}</span>
|
|
366
|
+
<button
|
|
367
|
+
type="button"
|
|
368
|
+
className="inline-flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
|
|
369
|
+
onClick={() => setRoaming((current) => !current)}
|
|
370
|
+
aria-label={roamingLabel}
|
|
371
|
+
title={roamingLabel}
|
|
372
|
+
>
|
|
373
|
+
{roaming ? <Pause className="size-3.5" /> : <Play className="size-3.5" />}
|
|
374
|
+
</button>
|
|
375
|
+
</div>
|
|
376
|
+
<div className="mt-1 flex items-center justify-between gap-3 font-mono text-[10px] text-muted-foreground">
|
|
377
|
+
<span>{String(status.capturedCount)} captured</span>
|
|
378
|
+
<span>
|
|
379
|
+
{status.pendingCount > 0
|
|
380
|
+
? `${String(status.pendingCount)} active`
|
|
381
|
+
: roaming
|
|
382
|
+
? "Roaming"
|
|
383
|
+
: "Resting"}
|
|
384
|
+
</span>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
)}
|
|
388
|
+
</>
|
|
389
|
+
);
|
|
390
|
+
}
|