@rexvane/piweb 0.3.16 → 0.3.18
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +8 -11
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +6 -6
- package/.next/react-loadable-manifest.json +4 -16
- package/.next/required-server-files.js +1 -3
- package/.next/required-server-files.json +1 -3
- package/.next/routes-manifest.json +0 -20
- package/.next/server/app/_global-error/page.js +2 -2
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +4 -7
- package/.next/server/app/_not-found.segments/_full.segment.rsc +4 -7
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -5
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -5
- package/.next/server/app/api/agent/[id]/events/route.js +2 -2
- package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/[id]/route.js +1 -1
- package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
- package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/new/route.js +1 -1
- package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
- package/.next/server/app/api/config/route.js +1 -1
- package/.next/server/app/api/config/route.js.nft.json +1 -1
- package/.next/server/app/api/files/route.js +1 -1
- package/.next/server/app/api/files/route.js.nft.json +1 -1
- package/.next/server/app/api/git/route.js +2 -2
- package/.next/server/app/api/git/route.js.nft.json +1 -1
- package/.next/server/app/api/growth/route.js +1 -1
- package/.next/server/app/api/growth/route.js.nft.json +1 -1
- package/.next/server/app/api/health/route.js +1 -1
- package/.next/server/app/api/models/route.js +1 -1
- package/.next/server/app/api/models/route.js.nft.json +1 -1
- package/.next/server/app/api/pi-settings/route.js +1 -1
- package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
- package/.next/server/app/api/plugins/route.js +1 -1
- package/.next/server/app/api/plugins/route.js.nft.json +1 -1
- package/.next/server/app/api/prompts/route.js +1 -1
- package/.next/server/app/api/prompts/route.js.nft.json +1 -1
- package/.next/server/app/api/security/route.js +1 -1
- package/.next/server/app/api/security/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/import/route.js +3 -3
- package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/route.js +2 -2
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/update/route.js +2 -2
- package/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/server/app/api/version/route.js +1 -1
- package/.next/server/app/api/version/route.js.nft.json +1 -1
- package/.next/server/app/api/web-auth/route.js +1 -1
- package/.next/server/app/api/workspaces/route.js +1 -1
- package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
- package/.next/server/app/icon.svg/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +6 -9
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -7
- package/.next/server/app/index.segments/_full.segment.rsc +6 -9
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -5
- package/.next/server/app/login/page.js +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +1 -1
- package/.next/server/app/login.rsc +5 -8
- package/.next/server/app/login.segments/_full.segment.rsc +5 -8
- package/.next/server/app/login.segments/_tree.segment.rsc +2 -5
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +3 -6
- package/.next/server/app/page.js +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +8 -11
- package/.next/server/chunks/496.js +1 -1
- package/.next/server/chunks/532.js +1 -1
- package/.next/server/chunks/686.js +4 -8
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/2608.ec8ff1f5767c9056.js +1 -0
- package/.next/static/chunks/2716.93be8ad5c970c516.js +1 -0
- package/.next/static/chunks/4632.112d2e893d48d188.js +1 -0
- package/.next/static/chunks/4673.c1fb1580efc6ace7.js +1 -0
- package/.next/static/chunks/4686-f4a037d609cea3f2.js +1 -0
- package/.next/static/chunks/app/_global-error/page-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/events/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/tree/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/agent/new/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/config/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/files/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/git/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/growth/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/health/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/models/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/pi-settings/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/plugins/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/prompts/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/security/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/export/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/sessions/import/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/sessions/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/skills/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/update/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/version/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/web-auth/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/workspaces/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/page-b51087831582ee37.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/webpack-c31263fd83d75ce6.js +1 -0
- package/.next/static/css/0cd968b9b62035b5.css +1 -0
- package/.next/static/en03hnPiuro0rnf8QkFEe/_buildManifest.js +1 -0
- package/.next/types/routes.d.ts +1 -4
- package/.next/types/validator.ts +0 -27
- package/README.md +17 -29
- package/assets/archive.jpg +0 -0
- package/assets/growth-1.jpg +0 -0
- package/assets/growth-2.jpg +0 -0
- package/assets/import.jpg +0 -0
- package/assets/main.jpg +0 -0
- package/assets/outline.jpg +0 -0
- package/assets/prompts-1.jpg +0 -0
- package/assets/prompts-2.jpg +0 -0
- package/assets/providers-1.jpg +0 -0
- package/assets/providers-2.jpg +0 -0
- package/assets/providers-3.jpg +0 -0
- package/assets/trace-1.jpg +0 -0
- package/assets/trace-2.png +0 -0
- package/assets/trace-3.png +0 -0
- package/assets/trace-4.png +0 -0
- package/assets/viewer-1.jpg +0 -0
- package/assets/viewer-2.jpg +0 -0
- package/bin/pi.js +50 -0
- package/next.config.ts +32 -7
- package/package.json +4 -9
- package/scripts/launcher.mjs +1 -1
- package/scripts/pick-folder.ps1 +2 -11
- package/scripts/release.mjs +31 -11
- package/scripts/verify-package.mjs +0 -53
- package/src/app/api/config/route.ts +1 -4
- package/src/app/api/growth/route.ts +14 -14
- package/src/app/api/models/route.ts +2 -1
- package/src/app/api/security/route.ts +3 -8
- package/src/app/api/update/route.ts +2 -8
- package/src/app/globals.css +92 -777
- package/src/app/layout.tsx +6 -13
- package/src/components/AppShell.tsx +229 -521
- package/src/components/ChatInput.tsx +26 -153
- package/src/components/ChatWindow.tsx +231 -51
- package/src/components/ContextMeter.tsx +17 -11
- package/src/components/FileViewer.tsx +11 -10
- package/src/components/GitPanel.tsx +11 -13
- package/src/components/ModelSelector.tsx +6 -19
- package/src/components/PiMark.tsx +38 -5
- package/src/components/ProjectPanel.tsx +111 -95
- package/src/components/PromptPanel.tsx +51 -132
- package/src/components/SessionImportSection.tsx +37 -53
- package/src/components/SessionSidebar.tsx +57 -159
- package/src/components/SettingsPanel.tsx +193 -330
- package/src/components/icons.tsx +0 -23
- package/src/hooks/useGrowth.ts +123 -156
- package/src/hooks/usePiWeb.ts +15 -122
- package/src/i18n.tsx +42 -98
- package/src/lib/agent-manager.ts +21 -187
- package/src/lib/command-validation.ts +1 -9
- package/src/lib/growth-service.ts +390 -166
- package/src/lib/growth-tracker.ts +64 -264
- package/src/lib/growth-turns.ts +31 -1
- package/src/lib/models/model-scope.ts +15 -0
- package/src/lib/models-service.ts +16 -0
- package/src/lib/pi-settings.ts +7 -0
- package/src/lib/pi.ts +2 -3
- package/src/lib/session-import/claude.ts +2 -13
- package/src/lib/session-import/codex.ts +8 -44
- package/src/lib/session-import/files.ts +0 -14
- package/src/lib/session-import/grok.ts +24 -54
- package/src/lib/session-import/index.ts +0 -5
- package/src/lib/session-import/types.ts +0 -12
- package/src/lib/session-import/writer.ts +3 -8
- package/src/lib/skills-service.ts +9 -16
- package/src/lib/theme.ts +1 -68
- package/src/lib/trajectory.ts +0 -1
- package/src/lib/types.ts +27 -36
- package/tsconfig.json +2 -1
- package/.next/server/app/api/swarm/[id]/route.js +0 -1
- package/.next/server/app/api/swarm/[id]/route.js.nft.json +0 -1
- package/.next/server/app/api/swarm/[id]/route_client-reference-manifest.js +0 -1
- package/.next/server/app/api/swarm/route.js +0 -1
- package/.next/server/app/api/swarm/route.js.nft.json +0 -1
- package/.next/server/app/api/swarm/route_client-reference-manifest.js +0 -1
- package/.next/server/app/api/tests/route.js +0 -2
- package/.next/server/app/api/tests/route.js.nft.json +0 -1
- package/.next/server/app/api/tests/route_client-reference-manifest.js +0 -1
- package/.next/server/chunks/757.js +0 -1
- package/.next/server/chunks/775.js +0 -3
- package/.next/static/0t1jBUAuVaTFTXxRyIo3q/_buildManifest.js +0 -1
- package/.next/static/chunks/2608.43c58236c9cce8e6.js +0 -1
- package/.next/static/chunks/2716.4ec42df1d51354c9.js +0 -1
- package/.next/static/chunks/4632.62139b565b657aac.js +0 -1
- package/.next/static/chunks/4673.7796104395deb8d7.js +0 -1
- package/.next/static/chunks/4686-17f21a006739a596.js +0 -1
- package/.next/static/chunks/7577.10af9bb661652da8.js +0 -1
- package/.next/static/chunks/8866.38ec4ac4ee5a1b74.js +0 -1
- package/.next/static/chunks/app/_global-error/page-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/events/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/tree/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/agent/new/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/config/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/files/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/git/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/growth/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/health/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/models/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/pi-settings/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/plugins/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/prompts/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/security/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/export/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/sessions/import/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/skills/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/swarm/[id]/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/swarm/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/tests/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/update/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/version/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/web-auth/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/workspaces/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/page-14379b8060d5cf07.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/webpack-d1a69ad9edb38b11.js +0 -1
- package/.next/static/css/d0a0162cb481abe8.css +0 -1
- package/.next/types/app/api/swarm/[id]/route.ts +0 -353
- package/.next/types/app/api/swarm/route.ts +0 -353
- package/.next/types/app/api/tests/route.ts +0 -353
- package/assets/archive.png +0 -0
- package/assets/growth-1.png +0 -0
- package/assets/growth-2.png +0 -0
- package/assets/import.png +0 -0
- package/assets/main.png +0 -0
- package/assets/outline.png +0 -0
- package/assets/prompts-1.png +0 -0
- package/assets/prompts-2.png +0 -0
- package/assets/providers-1.png +0 -0
- package/assets/providers-2.png +0 -0
- package/assets/providers-3.png +0 -0
- package/assets/trace-1.png +0 -0
- package/assets/viewer-1.png +0 -0
- package/assets/viewer-2.png +0 -0
- package/scripts/managed-niubash-assets.mjs +0 -146
- package/scripts/prepare-niubash-packages.mjs +0 -182
- package/scripts/run-test-file.mjs +0 -34
- package/scripts/smoke-managed-niubash.mjs +0 -62
- package/scripts/verify-release-config.mjs +0 -24
- package/src/app/api/swarm/[id]/route.ts +0 -28
- package/src/app/api/swarm/route.ts +0 -24
- package/src/app/api/tests/route.ts +0 -27
- package/src/components/SwarmCoordinatorPod.tsx +0 -1021
- package/src/components/TestInspectorPod.tsx +0 -549
- package/src/lib/delegation.ts +0 -93
- package/src/lib/growth-rounds.ts +0 -84
- package/src/lib/managed-niubash.ts +0 -231
- package/src/lib/runtime-activity.ts +0 -27
- package/src/lib/session-import/lock.ts +0 -25
- package/src/lib/swarm-service.ts +0 -371
- package/src/lib/test-run-service.ts +0 -122
- package/src/lib/workflow.ts +0 -55
- /package/.next/static/{0t1jBUAuVaTFTXxRyIo3q → en03hnPiuro0rnf8QkFEe}/_ssgManifest.js +0 -0
|
@@ -1,549 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
-
import type { TestRunResult } from "@/lib/test-run-service";
|
|
5
|
-
import { highlightLine, languageForPath, extOf } from "@/lib/highlight";
|
|
6
|
-
import {
|
|
7
|
-
IconCheckOutline14,
|
|
8
|
-
IconCloseOutline14,
|
|
9
|
-
IconFolderOpenOutline16,
|
|
10
|
-
IconRefreshOutline14,
|
|
11
|
-
} from "@/components/icons";
|
|
12
|
-
|
|
13
|
-
type Selection = { cwd: string; file: string };
|
|
14
|
-
type Catalog = { cwd: string; files: string[] };
|
|
15
|
-
type Source = Selection & { content: string };
|
|
16
|
-
type RunRecord = Selection & { result: TestRunResult };
|
|
17
|
-
type RunFailure = Selection & { message: string };
|
|
18
|
-
|
|
19
|
-
function duration(ms: number): string {
|
|
20
|
-
return ms >= 1000 ? `${(ms / 1000).toFixed(2)}s` : `${Math.round(ms)}ms`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function TestInspectorPod({
|
|
24
|
-
cwd,
|
|
25
|
-
onClose,
|
|
26
|
-
onSwitchToWorkbench,
|
|
27
|
-
onSwitchToDiff,
|
|
28
|
-
}: {
|
|
29
|
-
cwd: string;
|
|
30
|
-
onClose?: () => void;
|
|
31
|
-
onSwitchToWorkbench?: () => void;
|
|
32
|
-
onSwitchToDiff?: () => void;
|
|
33
|
-
}) {
|
|
34
|
-
const [catalog, setCatalog] = useState<Catalog | null>(null);
|
|
35
|
-
const [selection, setSelection] = useState<Selection | null>(null);
|
|
36
|
-
const [source, setSource] = useState<Source | null>(null);
|
|
37
|
-
const [listError, setListError] = useState("");
|
|
38
|
-
const [sourceError, setSourceError] = useState("");
|
|
39
|
-
const [loadingFiles, setLoadingFiles] = useState(false);
|
|
40
|
-
const [loadingSource, setLoadingSource] = useState(false);
|
|
41
|
-
const [refreshKey, setRefreshKey] = useState(0);
|
|
42
|
-
const [tab, setTab] = useState<"content" | "results">("content");
|
|
43
|
-
const [pendingRun, setPendingRun] = useState<Selection | null>(null);
|
|
44
|
-
const [runRecord, setRunRecord] = useState<RunRecord | null>(null);
|
|
45
|
-
const [runFailure, setRunFailure] = useState<RunFailure | null>(null);
|
|
46
|
-
const runInFlight = useRef(false);
|
|
47
|
-
|
|
48
|
-
const files = catalog?.cwd === cwd ? catalog.files : [];
|
|
49
|
-
const file = selection?.cwd === cwd && files.includes(selection.file) ? selection.file : "";
|
|
50
|
-
const visibleSource = source?.cwd === cwd && source.file === file ? source.content : null;
|
|
51
|
-
const visibleRun = runRecord?.cwd === cwd && runRecord.file === file ? runRecord.result : null;
|
|
52
|
-
const visibleRunError = runFailure?.cwd === cwd && runFailure.file === file ? runFailure.message : "";
|
|
53
|
-
const isRunning = pendingRun !== null;
|
|
54
|
-
|
|
55
|
-
useEffect(() => {
|
|
56
|
-
const controller = new AbortController();
|
|
57
|
-
setCatalog(null);
|
|
58
|
-
setSource(null);
|
|
59
|
-
setListError("");
|
|
60
|
-
if (!cwd) return () => controller.abort();
|
|
61
|
-
setLoadingFiles(true);
|
|
62
|
-
void (async () => {
|
|
63
|
-
try {
|
|
64
|
-
const response = await fetch(`/api/tests?cwd=${encodeURIComponent(cwd)}`, { signal: controller.signal });
|
|
65
|
-
const body = await response.json();
|
|
66
|
-
if (!response.ok || !body.success) throw new Error(body.error || "读取测试文件失败");
|
|
67
|
-
if (controller.signal.aborted) return;
|
|
68
|
-
const nextFiles = body.data.files as string[];
|
|
69
|
-
setCatalog({ cwd, files: nextFiles });
|
|
70
|
-
setSelection((previous) =>
|
|
71
|
-
previous?.cwd === cwd && nextFiles.includes(previous.file)
|
|
72
|
-
? previous
|
|
73
|
-
: nextFiles.length
|
|
74
|
-
? { cwd, file: nextFiles[0] }
|
|
75
|
-
: null,
|
|
76
|
-
);
|
|
77
|
-
} catch (error) {
|
|
78
|
-
if (!controller.signal.aborted) setListError(error instanceof Error ? error.message : "读取测试文件失败");
|
|
79
|
-
} finally {
|
|
80
|
-
if (!controller.signal.aborted) setLoadingFiles(false);
|
|
81
|
-
}
|
|
82
|
-
})();
|
|
83
|
-
return () => controller.abort();
|
|
84
|
-
}, [cwd, refreshKey]);
|
|
85
|
-
|
|
86
|
-
useEffect(() => {
|
|
87
|
-
const controller = new AbortController();
|
|
88
|
-
setSource(null);
|
|
89
|
-
setSourceError("");
|
|
90
|
-
if (!cwd || !file) return () => controller.abort();
|
|
91
|
-
setLoadingSource(true);
|
|
92
|
-
void (async () => {
|
|
93
|
-
try {
|
|
94
|
-
const response = await fetch(
|
|
95
|
-
`/api/tests?cwd=${encodeURIComponent(cwd)}&file=${encodeURIComponent(file)}`,
|
|
96
|
-
{ signal: controller.signal },
|
|
97
|
-
);
|
|
98
|
-
const body = await response.json();
|
|
99
|
-
if (!response.ok || !body.success) throw new Error(body.error || "读取测试内容失败");
|
|
100
|
-
if (!controller.signal.aborted) setSource({ cwd, file, content: body.data.content });
|
|
101
|
-
} catch (error) {
|
|
102
|
-
if (!controller.signal.aborted) setSourceError(error instanceof Error ? error.message : "读取测试内容失败");
|
|
103
|
-
} finally {
|
|
104
|
-
if (!controller.signal.aborted) setLoadingSource(false);
|
|
105
|
-
}
|
|
106
|
-
})();
|
|
107
|
-
return () => controller.abort();
|
|
108
|
-
}, [cwd, file]);
|
|
109
|
-
|
|
110
|
-
async function runSelectedFile() {
|
|
111
|
-
if (!cwd || !file || runInFlight.current) return;
|
|
112
|
-
runInFlight.current = true;
|
|
113
|
-
const target = { cwd, file };
|
|
114
|
-
setPendingRun(target);
|
|
115
|
-
setRunRecord(null);
|
|
116
|
-
setRunFailure(null);
|
|
117
|
-
setTab("results");
|
|
118
|
-
try {
|
|
119
|
-
const response = await fetch("/api/tests", {
|
|
120
|
-
method: "POST",
|
|
121
|
-
headers: { "Content-Type": "application/json" },
|
|
122
|
-
body: JSON.stringify(target),
|
|
123
|
-
});
|
|
124
|
-
const body = await response.json();
|
|
125
|
-
if (!response.ok || !body.success) throw new Error(body.error || "测试执行失败");
|
|
126
|
-
setRunRecord({ ...target, result: body.data as TestRunResult });
|
|
127
|
-
} catch (error) {
|
|
128
|
-
setRunFailure({ ...target, message: error instanceof Error ? error.message : "测试执行失败" });
|
|
129
|
-
} finally {
|
|
130
|
-
runInFlight.current = false;
|
|
131
|
-
setPendingRun(null);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const sourceLines = visibleSource !== null ? visibleSource.split("\n") : [];
|
|
136
|
-
const sourceBytes = visibleSource ? `${(visibleSource.length / 1024).toFixed(1)} KB` : "0 KB";
|
|
137
|
-
|
|
138
|
-
const fileLang = languageForPath(file) || "typescript";
|
|
139
|
-
const highlightedLines = useMemo(() => {
|
|
140
|
-
return sourceLines.map((line) => {
|
|
141
|
-
if (!line.trim()) return null;
|
|
142
|
-
return highlightLine(line, fileLang);
|
|
143
|
-
});
|
|
144
|
-
}, [sourceLines, fileLang]);
|
|
145
|
-
|
|
146
|
-
return (
|
|
147
|
-
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4 text-slate-800 dark:text-slate-100 antialiased">
|
|
148
|
-
{/* Top Breadcrumb & Live Test Status Bar (Screen 3) */}
|
|
149
|
-
<div className="clay-card flex flex-wrap items-center justify-between gap-3 rounded-2xl p-4 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 shadow-sm">
|
|
150
|
-
<div className="flex min-w-0 flex-wrap items-center gap-2.5">
|
|
151
|
-
<div className="clay-inset flex items-center gap-2 rounded-full px-3.5 py-1.5 text-xs font-bold text-slate-800 dark:text-slate-100">
|
|
152
|
-
<span className="w-2.5 h-2.5 rounded-full bg-emerald-500 animate-pulse" />
|
|
153
|
-
<span>测试执行套件</span>
|
|
154
|
-
<span className="rounded-full bg-emerald-100 dark:bg-emerald-950/70 px-2 py-0.5 text-[10px] font-bold text-emerald-800 dark:text-emerald-200 border border-emerald-300 dark:border-emerald-700">
|
|
155
|
-
工作区 Vitest
|
|
156
|
-
</span>
|
|
157
|
-
</div>
|
|
158
|
-
|
|
159
|
-
<div className="clay-btn-soft flex items-center gap-1.5 rounded-full px-3 py-1 font-mono text-xs text-slate-800 dark:text-slate-200 max-w-xs truncate" title={file || cwd}>
|
|
160
|
-
<IconFolderOpenOutline16 size={13} className="text-amber-500 flex-none" />
|
|
161
|
-
<span className="text-slate-500 dark:text-slate-400 font-semibold">tests/</span>
|
|
162
|
-
<span className="font-bold text-slate-900 dark:text-slate-100 truncate">{file ? file.replace(/^tests\//, "") : "未选定"}</span>
|
|
163
|
-
</div>
|
|
164
|
-
|
|
165
|
-
{visibleRun ? (
|
|
166
|
-
<div className={`flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold border ${visibleRun.success ? "bg-emerald-50 text-emerald-800 border-emerald-300 dark:bg-emerald-950/50 dark:text-emerald-200 dark:border-emerald-800" : "bg-rose-50 text-rose-800 border-rose-300 dark:bg-rose-950/50 dark:text-rose-200 dark:border-rose-800"}`}>
|
|
167
|
-
<span className={`w-2 h-2 rounded-full ${visibleRun.success ? "bg-emerald-500 animate-pulse" : "bg-rose-500"}`} />
|
|
168
|
-
<span>{visibleRun.success ? `全部用例就绪 (${visibleRun.passed}/${visibleRun.tests.length} 通过)` : `${visibleRun.failed} 项用例未通过`}</span>
|
|
169
|
-
</div>
|
|
170
|
-
) : isRunning ? (
|
|
171
|
-
<div className="flex items-center gap-1.5 px-3 py-1 rounded-full bg-amber-50 text-amber-800 border border-amber-300 dark:bg-amber-950/50 dark:text-amber-200 dark:border-amber-800 text-xs font-bold">
|
|
172
|
-
<span className="w-2 h-2 rounded-full bg-amber-500 animate-spin" />
|
|
173
|
-
<span>测试执行中...</span>
|
|
174
|
-
</div>
|
|
175
|
-
) : (
|
|
176
|
-
<div className="flex items-center gap-1.5 px-3 py-1 rounded-full bg-sky-50 text-sky-800 border border-sky-300 dark:bg-sky-950/50 dark:text-sky-200 dark:border-sky-800 text-xs font-bold">
|
|
177
|
-
<span className="w-2 h-2 rounded-full bg-sky-500" />
|
|
178
|
-
<span>用例就绪待跑 ({files.length} 个文件)</span>
|
|
179
|
-
</div>
|
|
180
|
-
)}
|
|
181
|
-
</div>
|
|
182
|
-
|
|
183
|
-
{/* Quick Meta Chips & Actions */}
|
|
184
|
-
<div className="flex items-center gap-2">
|
|
185
|
-
{visibleRun && (
|
|
186
|
-
<div className="clay-inset flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-mono text-slate-600 dark:text-slate-300 font-semibold">
|
|
187
|
-
<span>耗时:</span>
|
|
188
|
-
<strong className="text-slate-900 dark:text-slate-100">{duration(visibleRun.durationMs)}</strong>
|
|
189
|
-
</div>
|
|
190
|
-
)}
|
|
191
|
-
<div className="clay-inset flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-mono text-slate-600 dark:text-slate-300 font-semibold">
|
|
192
|
-
<span>堆内存:</span>
|
|
193
|
-
<strong className="text-slate-900 dark:text-slate-100">未采集</strong>
|
|
194
|
-
</div>
|
|
195
|
-
<button
|
|
196
|
-
type="button"
|
|
197
|
-
className="w-8 h-8 rounded-full clay-btn-soft flex items-center justify-center text-slate-600 hover:text-slate-950 dark:text-slate-300 dark:hover:text-white transition-colors"
|
|
198
|
-
title="刷新文件"
|
|
199
|
-
onClick={() => setRefreshKey((key) => key + 1)}
|
|
200
|
-
disabled={!cwd || loadingFiles}
|
|
201
|
-
>
|
|
202
|
-
<IconRefreshOutline14 size={14} className={loadingFiles ? "piweb-spin" : undefined} />
|
|
203
|
-
</button>
|
|
204
|
-
{onClose && (
|
|
205
|
-
<button
|
|
206
|
-
type="button"
|
|
207
|
-
className="w-8 h-8 rounded-full clay-btn-soft flex items-center justify-center text-slate-600 hover:text-rose-600 dark:text-slate-300 transition-colors"
|
|
208
|
-
title="关闭"
|
|
209
|
-
onClick={onClose}
|
|
210
|
-
>
|
|
211
|
-
<IconCloseOutline14 size={14} />
|
|
212
|
-
</button>
|
|
213
|
-
)}
|
|
214
|
-
</div>
|
|
215
|
-
</div>
|
|
216
|
-
|
|
217
|
-
{!cwd && (
|
|
218
|
-
<div className="clay-inset rounded-2xl p-6 text-sm text-slate-600 dark:text-slate-300 font-medium text-center">
|
|
219
|
-
请先在工作台选择工作区,再查看或运行测试。
|
|
220
|
-
</div>
|
|
221
|
-
)}
|
|
222
|
-
|
|
223
|
-
{listError && (
|
|
224
|
-
<div role="alert" className="rounded-2xl bg-rose-50 p-4 text-sm text-rose-800 dark:bg-rose-950/40 dark:text-rose-300 font-semibold shadow-sm border border-rose-300 dark:border-rose-800">
|
|
225
|
-
{listError}
|
|
226
|
-
</div>
|
|
227
|
-
)}
|
|
228
|
-
|
|
229
|
-
{cwd && (
|
|
230
|
-
<div className="grid grid-cols-1 items-start gap-4 xl:grid-cols-12 min-w-0">
|
|
231
|
-
{/* Left Spec Navigation Pod (3 Cols on XL) */}
|
|
232
|
-
<div className="xl:col-span-3 min-w-0 flex flex-col gap-4">
|
|
233
|
-
{/* Test Target Details Card */}
|
|
234
|
-
<div className="clay-card rounded-2xl p-4 flex flex-col gap-3 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 shadow-sm">
|
|
235
|
-
<div className="flex items-center justify-between pb-2 border-b border-slate-200 dark:border-slate-800">
|
|
236
|
-
<div className="flex items-center gap-2 font-bold text-sm text-slate-900 dark:text-slate-100">
|
|
237
|
-
<span className="w-2.5 h-2.5 rounded-full bg-indigo-500" />
|
|
238
|
-
<span>测试用例组</span>
|
|
239
|
-
</div>
|
|
240
|
-
<span className="px-2 py-0.5 rounded-full bg-sky-100 dark:bg-sky-950/60 text-sky-800 dark:text-sky-300 text-[10px] font-bold border border-sky-300 dark:border-sky-800">
|
|
241
|
-
Vitest Runner
|
|
242
|
-
</span>
|
|
243
|
-
</div>
|
|
244
|
-
|
|
245
|
-
{/* Suite List Items */}
|
|
246
|
-
<div className="flex flex-col gap-2 pt-1 max-h-[28vh] overflow-y-auto pr-1">
|
|
247
|
-
{visibleRun?.tests?.length ? (
|
|
248
|
-
visibleRun.tests.map((test, idx) => (
|
|
249
|
-
<div key={`${test.name}:${idx}`} className="p-2.5 rounded-xl clay-card bg-surface flex flex-col gap-1 border border-slate-200 dark:border-slate-800">
|
|
250
|
-
<div className="flex items-center justify-between gap-1">
|
|
251
|
-
<span className="font-bold text-xs text-slate-900 dark:text-slate-100 flex items-center gap-1.5 truncate">
|
|
252
|
-
<IconCheckOutline14 size={13} className={test.status === "passed" ? "text-emerald-500 flex-none" : "text-rose-500 flex-none"} />
|
|
253
|
-
<span className="truncate" title={test.name}>执行单元 #{idx + 1}</span>
|
|
254
|
-
</span>
|
|
255
|
-
<span className="font-mono text-[10px] text-slate-500 dark:text-slate-400 font-semibold flex-none">{duration(test.durationMs)}</span>
|
|
256
|
-
</div>
|
|
257
|
-
<p className="text-[11px] text-slate-600 dark:text-slate-400 font-mono">{test.status.toUpperCase()} · 工作区进程</p>
|
|
258
|
-
</div>
|
|
259
|
-
))
|
|
260
|
-
) : (
|
|
261
|
-
<div className="clay-inset p-3.5 rounded-xl text-center text-xs text-slate-600 dark:text-slate-300 font-medium leading-relaxed">
|
|
262
|
-
点击「运行当前文件」后,此处将按用例粒度展示真实状态与耗时。
|
|
263
|
-
</div>
|
|
264
|
-
)}
|
|
265
|
-
</div>
|
|
266
|
-
|
|
267
|
-
<div className="pt-2 flex items-center justify-between text-slate-600 dark:text-slate-300 text-xs font-semibold border-t border-slate-200 dark:border-slate-800">
|
|
268
|
-
<span>覆盖率:未采集</span>
|
|
269
|
-
</div>
|
|
270
|
-
</div>
|
|
271
|
-
|
|
272
|
-
{/* Test Files List Card */}
|
|
273
|
-
<div className="clay-card rounded-2xl p-4 flex flex-col gap-3 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 shadow-sm">
|
|
274
|
-
<div className="flex items-center justify-between pb-2 border-b border-slate-200 dark:border-slate-800 text-sm font-bold text-slate-900 dark:text-slate-100">
|
|
275
|
-
<span>测试文件</span>
|
|
276
|
-
<span className="font-mono text-xs font-semibold text-slate-600 dark:text-slate-400">{files.length} 个文件</span>
|
|
277
|
-
</div>
|
|
278
|
-
{loadingFiles && <p className="text-xs text-slate-500 dark:text-slate-400 font-medium">正在读取测试文件...</p>}
|
|
279
|
-
{!loadingFiles && !listError && files.length === 0 && (
|
|
280
|
-
<p className="text-xs text-slate-500 dark:text-slate-400 font-medium">此工作区没有测试文件。</p>
|
|
281
|
-
)}
|
|
282
|
-
<div className="max-h-[38vh] space-y-1.5 overflow-y-auto pr-1">
|
|
283
|
-
{files.map((item) => (
|
|
284
|
-
<button
|
|
285
|
-
key={item}
|
|
286
|
-
type="button"
|
|
287
|
-
title={item}
|
|
288
|
-
className={`w-full break-all rounded-xl px-3 py-2 text-left font-mono text-xs transition-all ${
|
|
289
|
-
file === item
|
|
290
|
-
? "clay-card bg-emerald-50 dark:bg-emerald-950/60 font-bold text-emerald-800 dark:text-emerald-200 shadow-sm border border-emerald-300 dark:border-emerald-700"
|
|
291
|
-
: "text-slate-700 hover:text-slate-950 dark:text-slate-200 dark:hover:text-white font-medium hover:bg-slate-100 dark:hover:bg-slate-800/80"
|
|
292
|
-
}`}
|
|
293
|
-
onClick={() => setSelection({ cwd, file: item })}
|
|
294
|
-
>
|
|
295
|
-
{item}
|
|
296
|
-
</button>
|
|
297
|
-
))}
|
|
298
|
-
</div>
|
|
299
|
-
</div>
|
|
300
|
-
</div>
|
|
301
|
-
|
|
302
|
-
{/* Center & Right: Floating Claymorphism Inspector Window (9 Cols on XL) */}
|
|
303
|
-
<div className="xl:col-span-9 min-w-0 flex flex-col gap-4">
|
|
304
|
-
{/* Clay Extruded Window Shell */}
|
|
305
|
-
<div className="w-full clay-card rounded-2xl p-4 flex flex-col transition-all bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 shadow-md">
|
|
306
|
-
{/* Window Ribbon / Soft Tactile Tab Strip */}
|
|
307
|
-
<div className="w-full flex flex-wrap items-center justify-between gap-3 pb-3 border-b border-slate-200 dark:border-slate-800">
|
|
308
|
-
{/* Left Tab Stacks */}
|
|
309
|
-
<div className="flex items-center gap-2 flex-wrap">
|
|
310
|
-
<button
|
|
311
|
-
type="button"
|
|
312
|
-
className="flex items-center gap-1.5 px-3 py-1.5 rounded-full clay-btn-soft text-slate-800 dark:text-slate-100 text-xs font-bold shadow-sm"
|
|
313
|
-
onClick={onSwitchToWorkbench}
|
|
314
|
-
>
|
|
315
|
-
<IconFolderOpenOutline16 size={14} className="text-amber-500" />
|
|
316
|
-
<span>项目</span>
|
|
317
|
-
</button>
|
|
318
|
-
<div className="flex items-center gap-2 px-3.5 py-1.5 rounded-full clay-card bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-50 font-mono text-xs font-bold shadow-sm border border-slate-200 dark:border-slate-700">
|
|
319
|
-
<span className="w-2 h-2 rounded-full bg-emerald-500" />
|
|
320
|
-
<span className="truncate max-w-[200px]">{file ? file.split("/").pop() : "选择文件"}</span>
|
|
321
|
-
</div>
|
|
322
|
-
</div>
|
|
323
|
-
|
|
324
|
-
{/* Right Controls Pill Group */}
|
|
325
|
-
<div className="flex items-center gap-2">
|
|
326
|
-
<span className="px-2.5 py-1 rounded-full clay-inset font-mono text-[11px] text-slate-700 dark:text-slate-300 font-semibold">
|
|
327
|
-
{visibleRun ? "当前文件结果" : "尚无运行结果"}
|
|
328
|
-
</span>
|
|
329
|
-
<div className="clay-inset flex items-center p-0.5 rounded-full text-xs">
|
|
330
|
-
<button
|
|
331
|
-
type="button"
|
|
332
|
-
className={`px-3 py-1 rounded-full transition-colors ${tab === "content" ? "clay-card bg-white dark:bg-slate-800 font-bold text-slate-900 dark:text-slate-50 shadow-sm border border-slate-200 dark:border-slate-700" : "text-slate-600 hover:text-slate-950 dark:text-slate-400 dark:hover:text-white font-medium"}`}
|
|
333
|
-
onClick={() => setTab("content")}
|
|
334
|
-
>
|
|
335
|
-
源码
|
|
336
|
-
</button>
|
|
337
|
-
<button
|
|
338
|
-
type="button"
|
|
339
|
-
className={`px-3 py-1 rounded-full transition-colors ${tab === "results" ? "clay-card bg-white dark:bg-slate-800 font-bold text-slate-900 dark:text-slate-50 shadow-sm border border-slate-200 dark:border-slate-700" : "text-slate-600 hover:text-slate-950 dark:text-slate-400 dark:hover:text-white font-medium"}`}
|
|
340
|
-
onClick={() => setTab("results")}
|
|
341
|
-
>
|
|
342
|
-
运行结果
|
|
343
|
-
</button>
|
|
344
|
-
</div>
|
|
345
|
-
{onClose && (
|
|
346
|
-
<button
|
|
347
|
-
type="button"
|
|
348
|
-
className="w-7 h-7 rounded-full clay-btn-soft flex items-center justify-center text-slate-500 hover:text-rose-600 dark:text-slate-400 transition-colors"
|
|
349
|
-
title="关闭"
|
|
350
|
-
onClick={onClose}
|
|
351
|
-
>
|
|
352
|
-
<IconCloseOutline14 size={14} />
|
|
353
|
-
</button>
|
|
354
|
-
)}
|
|
355
|
-
</div>
|
|
356
|
-
</div>
|
|
357
|
-
|
|
358
|
-
{/* Spec Full Path Header Bar */}
|
|
359
|
-
<div className="w-full flex flex-wrap items-center justify-between px-4 py-2.5 my-2.5 rounded-xl clay-inset text-xs border border-slate-200 dark:border-slate-800">
|
|
360
|
-
<div className="flex items-center gap-2 font-mono overflow-hidden">
|
|
361
|
-
<h2 className="font-bold text-slate-900 dark:text-slate-100 truncate" title={file}>
|
|
362
|
-
{file || "选择测试文件"}
|
|
363
|
-
</h2>
|
|
364
|
-
<span className="px-2 py-0.5 rounded-full bg-emerald-100 dark:bg-emerald-950/70 text-emerald-800 dark:text-emerald-200 text-[10px] font-bold border border-emerald-300 dark:border-emerald-700">
|
|
365
|
-
{file ? fileLang : "未选择"}
|
|
366
|
-
</span>
|
|
367
|
-
</div>
|
|
368
|
-
<div className="flex items-center gap-3 text-slate-600 dark:text-slate-300 font-mono text-[11px] font-semibold">
|
|
369
|
-
<span className="text-amber-700 dark:text-amber-400 font-bold">无系统沙盒隔离</span>
|
|
370
|
-
<span>|</span>
|
|
371
|
-
<span>{sourceBytes} ({sourceLines.length} 行)</span>
|
|
372
|
-
</div>
|
|
373
|
-
</div>
|
|
374
|
-
|
|
375
|
-
{/* Embedded Code Editor / Test Runner Output Surface */}
|
|
376
|
-
{tab === "content" ? (
|
|
377
|
-
<div className="relative w-full rounded-2xl clay-code-window p-4 overflow-x-auto min-h-[380px]">
|
|
378
|
-
{sourceError && <p role="alert" className="text-xs text-rose-300 font-semibold mb-2">{sourceError}</p>}
|
|
379
|
-
{loadingSource && <p className="text-xs text-slate-400 mb-2">正在读取源码...</p>}
|
|
380
|
-
{sourceLines.length > 0 ? (
|
|
381
|
-
<table className="w-full border-collapse font-mono text-xs select-text min-w-[680px]">
|
|
382
|
-
<colgroup>
|
|
383
|
-
<col className="w-14" style={{ width: "3.5rem", minWidth: "3.5rem" }} />
|
|
384
|
-
<col />
|
|
385
|
-
</colgroup>
|
|
386
|
-
<tbody>
|
|
387
|
-
{sourceLines.map((line, idx) => (
|
|
388
|
-
<tr key={idx} className="group hover:bg-slate-800/80 transition-colors leading-relaxed">
|
|
389
|
-
<td className="w-14 select-none text-right pr-4 text-slate-500 font-mono text-xs font-semibold align-top whitespace-nowrap shrink-0">
|
|
390
|
-
{idx + 1}
|
|
391
|
-
</td>
|
|
392
|
-
<td className="whitespace-pre text-slate-100 align-top font-mono text-xs pr-4">
|
|
393
|
-
{line || " "}
|
|
394
|
-
</td>
|
|
395
|
-
</tr>
|
|
396
|
-
))}
|
|
397
|
-
</tbody>
|
|
398
|
-
</table>
|
|
399
|
-
) : (
|
|
400
|
-
<div className="py-12 text-center text-slate-400 font-mono">
|
|
401
|
-
{file ? "此文件为空或正在读取..." : "请从左侧选择测试文件进行审查。"}
|
|
402
|
-
</div>
|
|
403
|
-
)}
|
|
404
|
-
<div className="absolute right-4 top-4 hidden md:flex flex-col gap-1 items-end pointer-events-none opacity-90">
|
|
405
|
-
<span className="px-2 py-0.5 rounded-full bg-slate-800 text-slate-300 border border-slate-700 font-mono text-[10px] font-semibold">
|
|
406
|
-
{extOf(file || "test.ts").toUpperCase()} • UTF-8
|
|
407
|
-
</span>
|
|
408
|
-
<span className="px-2 py-0.5 rounded-full bg-sky-950/80 text-sky-300 border border-sky-800/60 font-mono text-[10px] font-semibold">
|
|
409
|
-
Vitest Source
|
|
410
|
-
</span>
|
|
411
|
-
</div>
|
|
412
|
-
</div>
|
|
413
|
-
) : (
|
|
414
|
-
<div className="space-y-3 min-h-[300px]">
|
|
415
|
-
{pendingRun?.cwd === cwd && pendingRun.file === file && (
|
|
416
|
-
<div role="status" className="clay-inset rounded-xl p-4 text-sm font-semibold flex items-center gap-2 text-slate-800 dark:text-slate-100 border border-slate-200 dark:border-slate-800">
|
|
417
|
-
<span className="w-3 h-3 rounded-full bg-emerald-500 animate-spin" />
|
|
418
|
-
<span>正在运行 {file},最长等待 180 秒...</span>
|
|
419
|
-
</div>
|
|
420
|
-
)}
|
|
421
|
-
|
|
422
|
-
{visibleRunError && (
|
|
423
|
-
<div role="alert" className="rounded-xl bg-rose-50 dark:bg-rose-950/40 p-4 text-sm text-rose-800 dark:text-rose-200 font-bold shadow-sm border border-rose-300 dark:border-rose-800">
|
|
424
|
-
{visibleRunError}
|
|
425
|
-
</div>
|
|
426
|
-
)}
|
|
427
|
-
|
|
428
|
-
{!visibleRun && !visibleRunError && !isRunning && (
|
|
429
|
-
<div className="clay-inset rounded-xl p-8 text-center text-sm text-slate-600 dark:text-slate-300 font-medium border border-slate-200 dark:border-slate-800">
|
|
430
|
-
尚未运行当前文件。点击下方「运行当前文件」即可就地执行。
|
|
431
|
-
</div>
|
|
432
|
-
)}
|
|
433
|
-
|
|
434
|
-
{visibleRun && (
|
|
435
|
-
<>
|
|
436
|
-
<div className={`rounded-xl px-4 py-3 text-sm font-bold shadow-sm border ${visibleRun.success ? "bg-emerald-50 text-emerald-800 border-emerald-300 dark:bg-emerald-950/50 dark:text-emerald-200 dark:border-emerald-800" : "bg-rose-50 text-rose-800 border-rose-300 dark:bg-rose-950/50 dark:text-rose-200 dark:border-rose-800"}`}>
|
|
437
|
-
{visibleRun.success ? "执行通过" : "执行失败"} · {visibleRun.passed} 通过 · {visibleRun.failed} 失败 · {visibleRun.skipped} 跳过 · {duration(visibleRun.durationMs)}
|
|
438
|
-
</div>
|
|
439
|
-
<div className="max-h-[42vh] space-y-2 overflow-y-auto pr-1">
|
|
440
|
-
{visibleRun.tests.map((test, index) => (
|
|
441
|
-
<div key={`${test.name}:${index}`} className="clay-inset flex items-start justify-between gap-3 rounded-xl px-3.5 py-2.5 text-xs border border-slate-200 dark:border-slate-800">
|
|
442
|
-
<span className="min-w-0 break-words font-bold text-slate-900 dark:text-slate-100" title={test.name}>{test.name}</span>
|
|
443
|
-
<span className={`shrink-0 font-mono font-bold ${test.status === "passed" ? "text-emerald-700 dark:text-emerald-300" : test.status === "failed" ? "text-rose-700 dark:text-rose-300" : "text-slate-600"}`}>
|
|
444
|
-
{test.status} · {duration(test.durationMs)}
|
|
445
|
-
</span>
|
|
446
|
-
</div>
|
|
447
|
-
))}
|
|
448
|
-
</div>
|
|
449
|
-
{visibleRun.output && (
|
|
450
|
-
<details className="clay-inset rounded-xl p-3 text-xs border border-slate-200 dark:border-slate-800">
|
|
451
|
-
<summary className="cursor-pointer font-bold text-slate-800 dark:text-slate-100">运行输出</summary>
|
|
452
|
-
<pre className="mt-2 max-h-80 overflow-auto whitespace-pre-wrap break-all font-mono select-text clay-code-window p-3 rounded-xl">{visibleRun.output}</pre>
|
|
453
|
-
</details>
|
|
454
|
-
)}
|
|
455
|
-
</>
|
|
456
|
-
)}
|
|
457
|
-
</div>
|
|
458
|
-
)}
|
|
459
|
-
|
|
460
|
-
{/* Tactile Floating Workbench Action Dock */}
|
|
461
|
-
<div className="w-full flex flex-wrap items-center justify-between gap-3 pt-3 mt-3 border-t border-slate-200 dark:border-slate-800">
|
|
462
|
-
{/* Left Status & Diff Counter */}
|
|
463
|
-
<div className="flex items-center gap-3">
|
|
464
|
-
<div className="flex items-center gap-1.5 px-3 py-1.5 rounded-full clay-inset text-slate-700 dark:text-slate-200 text-xs font-mono font-medium border border-slate-200 dark:border-slate-800">
|
|
465
|
-
<IconCheckOutline14 size={13} className="text-emerald-500" />
|
|
466
|
-
<span>断言通过: <strong className="text-slate-950 dark:text-white font-bold">{visibleRun ? `${visibleRun.passed}/${visibleRun.tests.length}` : "—"}</strong></span>
|
|
467
|
-
</div>
|
|
468
|
-
</div>
|
|
469
|
-
|
|
470
|
-
{/* Right Action Buttons */}
|
|
471
|
-
<div className="flex items-center gap-2 flex-wrap">
|
|
472
|
-
<button
|
|
473
|
-
type="button"
|
|
474
|
-
className="px-3.5 py-1.5 rounded-full clay-btn-soft text-slate-800 hover:text-slate-950 dark:text-slate-200 dark:hover:text-white text-xs font-bold shadow-sm"
|
|
475
|
-
onClick={() => (onSwitchToDiff ? onSwitchToDiff() : onSwitchToWorkbench?.())}
|
|
476
|
-
>
|
|
477
|
-
改动对比
|
|
478
|
-
</button>
|
|
479
|
-
<button
|
|
480
|
-
type="button"
|
|
481
|
-
className="clay-btn clay-btn-mint px-4 py-2 rounded-full text-xs font-bold text-white flex items-center gap-1.5 disabled:opacity-50 disabled:cursor-not-allowed shadow-md"
|
|
482
|
-
onClick={() => void runSelectedFile()}
|
|
483
|
-
disabled={!file || isRunning}
|
|
484
|
-
>
|
|
485
|
-
<span>{isRunning ? "执行中..." : "运行当前文件"}</span>
|
|
486
|
-
</button>
|
|
487
|
-
<button
|
|
488
|
-
type="button"
|
|
489
|
-
className="px-3.5 py-1.5 rounded-full clay-btn-soft text-emerald-800 hover:text-emerald-950 dark:text-emerald-300 dark:hover:text-emerald-100 font-bold text-xs shadow-sm"
|
|
490
|
-
onClick={onSwitchToWorkbench}
|
|
491
|
-
>
|
|
492
|
-
工作台
|
|
493
|
-
</button>
|
|
494
|
-
</div>
|
|
495
|
-
</div>
|
|
496
|
-
</div>
|
|
497
|
-
|
|
498
|
-
{/* Execution Real-time Telemetry & Micro Console (3 Pods from Screen 3) */}
|
|
499
|
-
<div className="w-full grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
500
|
-
{/* Pod 1: Isolated FS State */}
|
|
501
|
-
<div className="clay-card rounded-2xl p-3.5 flex flex-col gap-1.5 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 shadow-sm">
|
|
502
|
-
<div className="flex items-center justify-between">
|
|
503
|
-
<div className="flex items-center gap-1.5 text-xs font-bold text-slate-900 dark:text-slate-100">
|
|
504
|
-
<IconFolderOpenOutline16 size={14} className="text-sky-500" />
|
|
505
|
-
<span>实际工作区</span>
|
|
506
|
-
</div>
|
|
507
|
-
<span className="px-2 py-0.5 rounded-full bg-sky-100 text-sky-800 dark:bg-sky-950/60 dark:text-sky-300 text-[10px] font-bold border border-sky-300 dark:border-sky-800">{cwd ? "已选择" : "未选择"}</span>
|
|
508
|
-
</div>
|
|
509
|
-
<p className="font-mono text-[11px] text-slate-600 dark:text-slate-400 truncate" title={cwd}>{cwd}</p>
|
|
510
|
-
<p className="text-[11px] text-slate-600 dark:text-slate-400">测试直接使用此目录,不创建临时文件系统沙盒。</p>
|
|
511
|
-
</div>
|
|
512
|
-
|
|
513
|
-
{/* Pod 2: Security & Permissions Barrier */}
|
|
514
|
-
<div className="clay-card rounded-2xl p-3.5 flex flex-col gap-1.5 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 shadow-sm">
|
|
515
|
-
<div className="flex items-center justify-between">
|
|
516
|
-
<div className="flex items-center gap-1.5 text-xs font-bold text-slate-900 dark:text-slate-100">
|
|
517
|
-
<span className="w-2 h-2 rounded-full bg-emerald-500" />
|
|
518
|
-
<span>执行权限说明</span>
|
|
519
|
-
</div>
|
|
520
|
-
<span className="px-2 py-0.5 rounded-full bg-amber-100 text-amber-800 dark:bg-amber-950/60 dark:text-amber-300 text-[10px] font-bold border border-amber-300 dark:border-amber-800">仅限可信代码</span>
|
|
521
|
-
</div>
|
|
522
|
-
<p className="text-[11px] text-slate-600 dark:text-slate-400 leading-tight">测试继承服务进程环境变量,可访问文件和网络;PI_OFFLINE 不提供系统级网络隔离。</p>
|
|
523
|
-
</div>
|
|
524
|
-
|
|
525
|
-
{/* Pod 3: Vitest Worker Cluster */}
|
|
526
|
-
<div className="clay-card rounded-2xl p-3.5 flex flex-col gap-1.5 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 shadow-sm">
|
|
527
|
-
<div className="flex items-center justify-between">
|
|
528
|
-
<div className="flex items-center gap-1.5 text-xs font-bold text-slate-900 dark:text-slate-100">
|
|
529
|
-
<span className="w-2 h-2 rounded-full bg-amber-500" />
|
|
530
|
-
<span>运行状态</span>
|
|
531
|
-
</div>
|
|
532
|
-
<span className="px-2 py-0.5 rounded-full bg-amber-100 text-amber-800 dark:bg-amber-950/60 dark:text-amber-300 text-[10px] font-bold border border-amber-300 dark:border-amber-800">{isRunning ? "执行中" : visibleRun ? (visibleRun.success ? "已通过" : "已失败") : "未运行"}</span>
|
|
533
|
-
</div>
|
|
534
|
-
<div className="flex items-center justify-between text-[11px] font-mono text-slate-700 dark:text-slate-300 font-semibold pt-0.5">
|
|
535
|
-
<span>Worker / PID</span>
|
|
536
|
-
<span>未采集</span>
|
|
537
|
-
</div>
|
|
538
|
-
<div className="flex items-center justify-between text-[11px] font-mono text-slate-700 dark:text-slate-300 font-semibold">
|
|
539
|
-
<span>执行时限</span>
|
|
540
|
-
<span>180 秒</span>
|
|
541
|
-
</div>
|
|
542
|
-
</div>
|
|
543
|
-
</div>
|
|
544
|
-
</div>
|
|
545
|
-
</div>
|
|
546
|
-
)}
|
|
547
|
-
</div>
|
|
548
|
-
);
|
|
549
|
-
}
|
package/src/lib/delegation.ts
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { DefaultResourceLoader, SessionManager, createAgentSession, defineTool, type AgentSession, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Type } from "typebox";
|
|
3
|
-
import { getAgentDir, getModelRuntime, getSettingsManager, TOOL_PRESETS, withResourceLock } from "./pi";
|
|
4
|
-
|
|
5
|
-
const MAX_TASKS = 3;
|
|
6
|
-
const TASK_TIMEOUT_MS = 120_000;
|
|
7
|
-
const MAX_REPORT_CHARS = 12_000;
|
|
8
|
-
|
|
9
|
-
const parameters = Type.Object({
|
|
10
|
-
tasks: Type.Array(Type.Object({
|
|
11
|
-
title: Type.String({ minLength: 1, maxLength: 80 }),
|
|
12
|
-
instruction: Type.String({ minLength: 1, maxLength: 4_000 }),
|
|
13
|
-
}), { minItems: 1, maxItems: MAX_TASKS }),
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
function reportOf(session: AgentSession): string {
|
|
17
|
-
const answer = [...session.messages].reverse().find((message) => message.role === "assistant");
|
|
18
|
-
if (!answer || answer.role !== "assistant") return "The subagent returned no assistant answer.";
|
|
19
|
-
return answer.content.filter((part) => part.type === "text").map((part) => part.text).join("\n").slice(0, MAX_REPORT_CHARS) || "The subagent returned no text answer.";
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function runSubagent(cwd: string, parent: AgentSession, instruction: string, signal?: AbortSignal): Promise<string> {
|
|
23
|
-
if (signal?.aborted) throw new Error("Delegation cancelled");
|
|
24
|
-
const loader = new DefaultResourceLoader({
|
|
25
|
-
cwd,
|
|
26
|
-
agentDir: getAgentDir(),
|
|
27
|
-
settingsManager: getSettingsManager(cwd),
|
|
28
|
-
noExtensions: true,
|
|
29
|
-
noPromptTemplates: true,
|
|
30
|
-
noThemes: true,
|
|
31
|
-
appendSystemPrompt: ["You are a read-only PiWeb subagent. Investigate the assigned task and return a concise, evidence-based report. Do not modify files or run commands. Do not delegate further."],
|
|
32
|
-
});
|
|
33
|
-
let child: AgentSession | undefined;
|
|
34
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
35
|
-
let timedOut = false;
|
|
36
|
-
const abort = () => { void child?.abort().catch(() => undefined); };
|
|
37
|
-
try {
|
|
38
|
-
await withResourceLock(() => loader.reload());
|
|
39
|
-
if (signal?.aborted) throw new Error("Delegation cancelled");
|
|
40
|
-
const created = await createAgentSession({
|
|
41
|
-
cwd,
|
|
42
|
-
agentDir: getAgentDir(),
|
|
43
|
-
sessionManager: SessionManager.inMemory(cwd),
|
|
44
|
-
modelRuntime: await getModelRuntime(),
|
|
45
|
-
model: parent.model ?? undefined,
|
|
46
|
-
thinkingLevel: parent.thinkingLevel,
|
|
47
|
-
settingsManager: getSettingsManager(cwd),
|
|
48
|
-
resourceLoader: loader,
|
|
49
|
-
tools: TOOL_PRESETS.readonly,
|
|
50
|
-
});
|
|
51
|
-
child = created.session;
|
|
52
|
-
if (signal?.aborted) throw new Error("Delegation cancelled");
|
|
53
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
54
|
-
timer = setTimeout(() => { timedOut = true; abort(); }, TASK_TIMEOUT_MS);
|
|
55
|
-
await child.prompt(instruction, { source: "rpc", expandPromptTemplates: false });
|
|
56
|
-
if (timedOut) throw new Error("Subagent timed out after two minutes");
|
|
57
|
-
if (signal?.aborted) throw new Error("Delegation cancelled");
|
|
58
|
-
return reportOf(child);
|
|
59
|
-
} finally {
|
|
60
|
-
if (timer) clearTimeout(timer);
|
|
61
|
-
signal?.removeEventListener("abort", abort);
|
|
62
|
-
child?.dispose();
|
|
63
|
-
loader.getExtensions().runtime.invalidate();
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function createDelegationTool(cwd: string, parent: () => AgentSession | null): ToolDefinition {
|
|
68
|
-
return defineTool({
|
|
69
|
-
name: "piweb_delegate",
|
|
70
|
-
label: "Delegate research",
|
|
71
|
-
description: "Delegate one to three independent, read-only research or review tasks to temporary Pi subagents. They cannot edit files, run shell commands, or delegate further. Use only when parallel investigation is useful; summarize their findings yourself.",
|
|
72
|
-
promptSnippet: "Delegate bounded, read-only research tasks to temporary subagents.",
|
|
73
|
-
parameters,
|
|
74
|
-
executionMode: "sequential",
|
|
75
|
-
async execute(_id, { tasks }, signal, onUpdate) {
|
|
76
|
-
const leader = parent();
|
|
77
|
-
if (!leader) throw new Error("Parent session is unavailable");
|
|
78
|
-
const reports: string[] = new Array(tasks.length);
|
|
79
|
-
await Promise.all(tasks.map(async (task, index) => {
|
|
80
|
-
try {
|
|
81
|
-
const report = await runSubagent(cwd, leader, task.instruction, signal);
|
|
82
|
-
reports[index] = `## ${task.title}\n${report}`;
|
|
83
|
-
} catch (error) {
|
|
84
|
-
reports[index] = `## ${task.title}\nFailed: ${String((error as Error)?.message ?? error)}`;
|
|
85
|
-
}
|
|
86
|
-
onUpdate?.({ content: [{ type: "text", text: reports.filter(Boolean).join("\n\n") }], details: undefined });
|
|
87
|
-
}));
|
|
88
|
-
if (signal?.aborted) throw new Error("Delegation cancelled");
|
|
89
|
-
const text = reports.join("\n\n");
|
|
90
|
-
return { content: [{ type: "text", text }], details: undefined };
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
}
|