@rexvane/piweb 0.3.17 → 0.3.19
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 +6 -4
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +4 -4
- package/.next/react-loadable-manifest.json +23 -9
- package/.next/required-server-files.js +2 -1
- package/.next/required-server-files.json +2 -1
- package/.next/routes-manifest.json +12 -0
- 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 +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/agent/[id]/events/route.js +3 -3
- 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/fs/route.js +1 -0
- package/.next/server/app/api/fs/route.js.nft.json +1 -0
- package/.next/server/app/api/fs/route_client-reference-manifest.js +1 -0
- 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/remote/route.js +8 -0
- package/.next/server/app/api/remote/route.js.nft.json +1 -0
- package/.next/server/app/api/remote/route_client-reference-manifest.js +1 -0
- 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 +2 -2
- 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 +1 -1
- 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 +3 -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 +4 -4
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/index.segments/_full.segment.rsc +4 -4
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- 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 +3 -3
- package/.next/server/app/login.segments/_full.segment.rsc +3 -3
- package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/page.js +1 -1
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +6 -4
- package/.next/server/chunks/229.js +1 -1
- package/.next/server/chunks/243.js +1 -0
- package/.next/server/chunks/393.js +1 -0
- package/.next/server/chunks/496.js +1 -1
- package/.next/server/chunks/538.js +1 -0
- package/.next/server/chunks/686.js +1 -4
- package/.next/server/chunks/802.js +2 -0
- package/.next/server/chunks/807.js +1 -0
- package/.next/server/chunks/920.js +4 -0
- package/.next/server/chunks/924.js +6 -0
- package/.next/server/instrumentation.js +1 -0
- package/.next/server/instrumentation.js.nft.json +1 -0
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/middleware.js +4 -3
- 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/JDGSR-TIbk9qZWcUNYIX3/_buildManifest.js +1 -0
- package/.next/static/chunks/2716.7ad073038e94a802.js +1 -0
- package/.next/static/chunks/316-5ea443ee6b512aa9.js +1 -0
- package/.next/static/chunks/4673.8a5a9e6855350e34.js +1 -0
- package/.next/static/chunks/4686-e3825f3b6a384aa1.js +1 -0
- package/.next/static/chunks/4778.e4c9ed093a413bc4.js +1 -0
- package/.next/static/chunks/5173.91ddb803785009f8.js +1 -0
- package/.next/static/chunks/839.7403c6bdace4c56c.js +1 -0
- package/.next/static/chunks/8552.6bfe3e101250b20f.js +1 -0
- package/.next/static/chunks/app/_global-error/page-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/events/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/tree/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/agent/new/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/config/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/files/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/fs/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/git/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/growth/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/health/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/models/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/pi-settings/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/plugins/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/prompts/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/remote/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/security/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/export/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/sessions/import/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/sessions/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/skills/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/update/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/version/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/web-auth/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/api/workspaces/route-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/app/login/page-5e276a1eeb8867df.js +1 -0
- package/.next/static/chunks/app/page-2e425ef83fba0782.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-b179d9f7f6f2daf9.js +1 -0
- package/.next/static/chunks/webpack-bd2b9202ecbcdcaa.js +1 -0
- package/.next/static/css/7391c42e1ae617f9.css +1 -0
- package/.next/types/app/api/fs/route.ts +353 -0
- package/.next/types/app/api/remote/route.ts +353 -0
- package/.next/types/routes.d.ts +3 -1
- package/.next/types/validator.ts +18 -0
- package/README.md +56 -10
- package/next.config.ts +3 -2
- package/package.json +3 -3
- package/scripts/launcher.mjs +194 -13
- package/scripts/release.mjs +0 -2
- package/src/app/api/agent/[id]/events/route.ts +11 -6
- package/src/app/api/fs/route.ts +119 -0
- package/src/app/api/growth/route.ts +11 -6
- package/src/app/api/models/route.ts +21 -9
- package/src/app/api/remote/route.ts +67 -0
- package/src/app/api/sessions/[id]/export/route.ts +11 -1
- package/src/app/api/sessions/route.ts +3 -1
- package/src/app/api/web-auth/route.ts +45 -49
- package/src/app/api/workspaces/route.ts +16 -0
- package/src/app/globals.css +11 -3
- package/src/app/login/page.tsx +8 -60
- package/src/components/AppShell.tsx +202 -155
- package/src/components/ChatInput.tsx +61 -6
- package/src/components/ChatWindow.tsx +274 -132
- package/src/components/DirectoryPicker.tsx +189 -0
- package/src/components/DrawerPanel.tsx +164 -0
- package/src/components/FileViewer.tsx +2 -2
- package/src/components/GrowthTree.tsx +6 -5
- package/src/components/MarkdownBlock.tsx +134 -0
- package/src/components/OutlineRail.tsx +22 -1
- package/src/components/ProjectPanel.tsx +197 -14
- package/src/components/RemoteSection.tsx +232 -0
- package/src/components/SessionImportSection.tsx +60 -38
- package/src/components/SettingsPanel.tsx +104 -57
- package/src/components/icons.tsx +23 -0
- package/src/hooks/useGrowth.ts +47 -25
- package/src/hooks/usePiWeb.ts +100 -14
- package/src/i18n.tsx +115 -28
- package/src/instrumentation-node.ts +35 -0
- package/src/instrumentation.ts +18 -0
- package/src/lib/agent-config.ts +134 -0
- package/src/lib/agent-manager.ts +243 -61
- package/src/lib/auth-throttle.ts +102 -0
- package/src/lib/auth.ts +35 -27
- package/src/lib/files-service.ts +61 -1
- package/src/lib/font.ts +84 -0
- package/src/lib/git-service.ts +5 -3
- package/src/lib/growth-service.ts +78 -30
- package/src/lib/growth-tracker.ts +91 -9
- package/src/lib/models-service.ts +36 -11
- package/src/lib/pi-settings.ts +10 -12
- package/src/lib/pi.ts +145 -18
- package/src/lib/plugins-service.ts +67 -35
- package/src/lib/remote/assets.json +4 -0
- package/src/lib/remote/mobile-proxy.ts +149 -0
- package/src/lib/remote/supervisor.ts +482 -0
- package/src/lib/security-service.ts +6 -11
- package/src/lib/session-import/claude.ts +45 -21
- package/src/lib/session-reader.ts +106 -3
- package/src/lib/skills-service.ts +34 -12
- package/src/lib/text-sanitize.ts +12 -0
- package/src/lib/theme.ts +3 -0
- package/src/lib/types.ts +16 -2
- package/src/lib/update-service.ts +0 -1
- package/src/lib/web-auth-shared.ts +5 -1
- package/src/lib/web-auth.ts +139 -22
- package/src/lib/web-launch-token.ts +55 -0
- package/src/lib/web-secret.ts +182 -0
- package/src/lib/workspace-store.ts +91 -7
- package/src/proxy.ts +46 -12
- package/.next/server/chunks/532.js +0 -1
- package/.next/static/chunks/2155.2f27fe7098987699.js +0 -1
- package/.next/static/chunks/2608.ec8ff1f5767c9056.js +0 -1
- package/.next/static/chunks/2716.93be8ad5c970c516.js +0 -1
- package/.next/static/chunks/4632.112d2e893d48d188.js +0 -1
- package/.next/static/chunks/4673.c1fb1580efc6ace7.js +0 -1
- package/.next/static/chunks/4686-f4a037d609cea3f2.js +0 -1
- package/.next/static/chunks/692-7992db0ae9cc3021.js +0 -1
- package/.next/static/chunks/app/_global-error/page-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/events/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/tree/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/agent/new/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/config/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/files/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/git/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/growth/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/health/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/models/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/pi-settings/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/plugins/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/prompts/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/security/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/export/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/sessions/import/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/skills/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/update/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/version/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/web-auth/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/workspaces/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/login/page-52ef4841cbd199a8.js +0 -1
- package/.next/static/chunks/app/page-b51087831582ee37.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/webpack-c31263fd83d75ce6.js +0 -1
- package/.next/static/css/850c2fd130ffa2fc.css +0 -1
- package/.next/static/zsynuO_9Kx6nrXgkTUJ04/_buildManifest.js +0 -1
- /package/.next/static/{zsynuO_9Kx6nrXgkTUJ04 → JDGSR-TIbk9qZWcUNYIX3}/_ssgManifest.js +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* 项目栏:右侧抽屉的「项目」入口(不再独占网格一列)。项目生长 = 每轮一个 git commit。
|
|
5
5
|
* 头部:工作区名 + 立即记录 / 收起;所选轮:提问首行、时间、文件数与行数、在对话里看;
|
|
6
6
|
* 工具行:范围(本轮 / 本会话累计)、只看改动、筛选;中间:项目树(GrowthTree);
|
|
7
|
-
* 底部:时间轴每轮一根柱(高度 = 改动行数,灰色细柱 =
|
|
7
|
+
* 底部:时间轴每轮一根柱(高度 = 改动行数,灰色细柱 = 你在两轮之间自己的修改),默认不再跟随,底部计数可点开轮数列表跳到任意一轮。
|
|
8
8
|
*/
|
|
9
9
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
10
10
|
import { GrowthTree } from "@/components/GrowthTree";
|
|
@@ -13,7 +13,59 @@ import type { GrowthApi, GrowthEntry } from "@/hooks/useGrowth";
|
|
|
13
13
|
import { useI18n } from "@/i18n";
|
|
14
14
|
import type { GitInfo } from "@/lib/git-service";
|
|
15
15
|
import type { TreeNode } from "@/lib/growth-tree";
|
|
16
|
-
import type { GrowthChange } from "@/lib/types";
|
|
16
|
+
import type { GrowthChange, GrowthConsentRequest } from "@/lib/types";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 「要不要在这个工作区建 git 仓库」:工作区还不是仓库根目录时,生长记录第一次提交前问一次。
|
|
20
|
+
* 关掉(Esc / 点遮罩外)不算回答,下次打开会话还会再问;只有两个按钮会被记住。
|
|
21
|
+
*/
|
|
22
|
+
export function GrowthConsentDialog({ request, onAnswer }: { request: GrowthConsentRequest; onAnswer: (allow: boolean) => void }) {
|
|
23
|
+
const { t } = useI18n();
|
|
24
|
+
const [dismissed, setDismissed] = useState(false);
|
|
25
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
setDismissed(false);
|
|
28
|
+
ref.current?.focus();
|
|
29
|
+
}, [request.cwd, request.reason]);
|
|
30
|
+
if (dismissed) return null;
|
|
31
|
+
const body = request.reason === "nested" ? t.growthConsentNested.replace("{parent}", request.parent ?? "") : t.growthConsentNotGit;
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
ref={ref}
|
|
35
|
+
tabIndex={-1}
|
|
36
|
+
className="fixed inset-0 z-[120] flex items-center justify-center"
|
|
37
|
+
style={{ background: "var(--dsw-mask)" }}
|
|
38
|
+
role="dialog"
|
|
39
|
+
aria-modal="true"
|
|
40
|
+
aria-label={t.growthConsentTitle}
|
|
41
|
+
data-testid="growth-consent-dialog"
|
|
42
|
+
onKeyDown={(event) => {
|
|
43
|
+
event.stopPropagation();
|
|
44
|
+
if (event.key === "Escape") setDismissed(true);
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
<div className="popover flex w-[min(92vw,480px)] flex-col gap-3 p-5" style={{ background: "var(--dsw-glass-modal)" }}>
|
|
48
|
+
<div className="flex items-start gap-2.5">
|
|
49
|
+
<span className="flex flex-none items-center justify-center rounded-lg" style={{ width: 30, height: 30, background: "var(--dsw-hover)" }}>
|
|
50
|
+
<IconGitOutline16 size={16} style={{ color: "var(--dsw-label-secondary)" }} />
|
|
51
|
+
</span>
|
|
52
|
+
<div className="min-w-0 flex-1">
|
|
53
|
+
<div style={{ fontSize: 14.5, fontWeight: 600, lineHeight: 1.4 }}>{t.growthConsentTitle}</div>
|
|
54
|
+
<div className="mt-0.5 truncate" style={{ fontSize: 11, color: "var(--dsw-label-caption)", fontFamily: "var(--font-mono)" }} title={request.cwd}>{request.cwd}</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
<div className="rounded-xl px-3 py-2.5" style={{ fontSize: 12.5, color: "var(--dsw-label-secondary)", lineHeight: 1.55, background: "var(--dsw-hover)" }}>
|
|
58
|
+
{body}
|
|
59
|
+
<div className="mt-1.5" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>{t.growthConsentNote}</div>
|
|
60
|
+
</div>
|
|
61
|
+
<div className="flex justify-end gap-2">
|
|
62
|
+
<button className="btn-outline" style={{ height: 32 }} onClick={() => onAnswer(false)}>{t.growthConsentDecline}</button>
|
|
63
|
+
<button className="btn-primary-white" style={{ height: 32 }} onClick={() => onAnswer(true)}>{t.growthConsentAllow}</button>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
17
69
|
|
|
18
70
|
/** 「第 7 轮 · 把保存按钮改成红色」/「你的修改」 */
|
|
19
71
|
export function roundTitle(round: GrowthEntry, t: Record<string, string>): string {
|
|
@@ -90,6 +142,13 @@ export function ProjectPanel({
|
|
|
90
142
|
const [onlyChanges, setOnlyChanges] = useState(true);
|
|
91
143
|
const [recording, setRecording] = useState(false);
|
|
92
144
|
const [notice, setNotice] = useState("");
|
|
145
|
+
/** 轮数列表(底部计数点开) */
|
|
146
|
+
const [roundPickerOpen, setRoundPickerOpen] = useState(false);
|
|
147
|
+
/** 列表顶部「输入轮号...」已就地把那一行变成输入框 */
|
|
148
|
+
const [jumpEditing, setJumpEditing] = useState(false);
|
|
149
|
+
const [jumpValue, setJumpValue] = useState("");
|
|
150
|
+
const roundPickerRef = useRef<HTMLDivElement>(null);
|
|
151
|
+
const roundListRef = useRef<HTMLDivElement>(null);
|
|
93
152
|
const [gitState, setGitState] = useState<{ cwd: string; info: GitInfo | null; error?: string } | null>(null);
|
|
94
153
|
const git = gitState?.cwd === cwd ? gitState.info : null;
|
|
95
154
|
const gitError = gitState?.cwd === cwd ? gitState.error ?? git?.statusError : undefined;
|
|
@@ -125,6 +184,43 @@ export function ProjectPanel({
|
|
|
125
184
|
railRef.current?.querySelector<HTMLElement>(`[data-commit="${selected.commit}"]`)?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
126
185
|
}, [selected]);
|
|
127
186
|
const selectedIdx = selected ? rounds.findIndex((round) => round.commit === selected.commit) : -1;
|
|
187
|
+
|
|
188
|
+
// 轮数列表:打开时自动滚到当前轮;Esc / 点外部关闭
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (!roundPickerOpen) {
|
|
191
|
+
setJumpEditing(false);
|
|
192
|
+
setJumpValue("");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
requestAnimationFrame(() => {
|
|
196
|
+
roundListRef.current?.querySelector('[data-selected]')?.scrollIntoView({ block: "center" });
|
|
197
|
+
});
|
|
198
|
+
const onPointerDown = (e: MouseEvent) => {
|
|
199
|
+
if (!roundPickerRef.current?.contains(e.target as Node)) setRoundPickerOpen(false);
|
|
200
|
+
};
|
|
201
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
202
|
+
if (e.key === "Escape") {
|
|
203
|
+
e.stopPropagation();
|
|
204
|
+
setRoundPickerOpen(false);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
document.addEventListener("mousedown", onPointerDown);
|
|
208
|
+
document.addEventListener("keydown", onKeyDown);
|
|
209
|
+
return () => {
|
|
210
|
+
document.removeEventListener("mousedown", onPointerDown);
|
|
211
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
212
|
+
};
|
|
213
|
+
}, [roundPickerOpen]);
|
|
214
|
+
/** 跳到第 n 轮(轮号从 1 起,只数 pi 的轮次);越界夹到有效范围,非法输入不跳 */
|
|
215
|
+
const jumpToRound = (raw: string) => {
|
|
216
|
+
const parsed = Number.parseInt(raw.trim(), 10);
|
|
217
|
+
if (Number.isNaN(parsed) || piRounds === 0) return;
|
|
218
|
+
const n = Math.max(1, Math.min(piRounds, parsed));
|
|
219
|
+
const round = rounds.find((item) => item.n === n);
|
|
220
|
+
if (!round) return;
|
|
221
|
+
growth.select(round.commit);
|
|
222
|
+
setRoundPickerOpen(false);
|
|
223
|
+
};
|
|
128
224
|
const conflicts = git?.files.filter((file) => file.kind === "conflict").length ?? 0;
|
|
129
225
|
const staged = git?.files.filter((file) => file.kind !== "conflict" && file.indexStatus !== " ").length ?? 0;
|
|
130
226
|
const unstaged = git?.files.filter((file) => file.kind !== "conflict" && file.workStatus !== " " && file.kind !== "untracked").length ?? 0;
|
|
@@ -152,8 +248,10 @@ export function ProjectPanel({
|
|
|
152
248
|
const emptyText = !cwd || !hasSession ? t.growthNoSession : growth.loading && !rounds.length ? t.growthLoading : !growth.available ? t.growthUnavailable : rounds.length ? undefined : t.growthNoRounds;
|
|
153
249
|
const statusText = selected?.status === "aborted" ? t.growthStatusAborted : selected?.status === "error" ? t.growthStatusError : "";
|
|
154
250
|
|
|
251
|
+
// 项目面板现在是抽屉的一个入口(不再独占网格一列):.pw-project 自带的右描边在抽屉里
|
|
252
|
+
// 会变成贴在窗口最右边的一条多余发丝线,所以这里去掉;背景沿用侧栏填充色即可。
|
|
155
253
|
return (
|
|
156
|
-
<div className="pw-project flex h-full min-h-0 flex-col" data-testid="project-panel">
|
|
254
|
+
<div className="pw-project flex h-full min-h-0 flex-1 flex-col" style={{ borderRight: "none" }} data-testid="project-panel">
|
|
157
255
|
{/* 头部 */}
|
|
158
256
|
<div className="flex items-center gap-1.5 px-3 pt-3 pb-1.5">
|
|
159
257
|
<span className="min-w-0 flex-1 truncate" style={{ fontSize: 13, fontWeight: 600 }} title={cwd}>
|
|
@@ -314,16 +412,101 @@ export function ProjectPanel({
|
|
|
314
412
|
<button className="icon-btn" style={{ width: 20, height: 20 }} title={t.growthNext} onClick={growth.next} disabled={selectedIdx < 0 || selectedIdx >= rounds.length - 1}>
|
|
315
413
|
<IconChevronRight14 size={12} />
|
|
316
414
|
</button>
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
415
|
+
{/* 计数可点开轮数列表:单击直接弹列表(不做双击变输入框,避免 250ms 单双击
|
|
416
|
+
判定拖慢弹出);「输入轮号」是列表里一个固定入口,选中后当场变数字输入框 */}
|
|
417
|
+
<div className="relative min-w-0 flex-1">
|
|
418
|
+
<button
|
|
419
|
+
type="button"
|
|
420
|
+
className="w-full truncate rounded px-1 text-left"
|
|
421
|
+
style={{ color: "var(--dsw-label-secondary)", background: roundPickerOpen ? "var(--dsw-active)" : undefined }}
|
|
422
|
+
title={t.growthPickRound}
|
|
423
|
+
aria-haspopup="listbox"
|
|
424
|
+
aria-expanded={roundPickerOpen}
|
|
425
|
+
data-testid="growth-round-picker"
|
|
426
|
+
onClick={() => setRoundPickerOpen((v) => !v)}
|
|
427
|
+
>
|
|
428
|
+
{selected?.n
|
|
429
|
+
? t.growthRoundPosition.replace("{current}", String(selected.n)).replace("{total}", String(piRounds))
|
|
430
|
+
: selected
|
|
431
|
+
? t.growthKindUser
|
|
432
|
+
: t.growthRoundPosition.replace("{current}", "0").replace("{total}", String(piRounds))}
|
|
433
|
+
</button>
|
|
434
|
+
{roundPickerOpen && (
|
|
435
|
+
<div
|
|
436
|
+
ref={roundPickerRef}
|
|
437
|
+
className="popover absolute bottom-7 left-0 z-50 flex w-[268px] max-w-[calc(100vw-24px)] flex-col"
|
|
438
|
+
role="listbox"
|
|
439
|
+
aria-label={t.growthPickRound}
|
|
440
|
+
data-testid="growth-round-list"
|
|
441
|
+
>
|
|
442
|
+
{/* 「输入轮号」入口固定在最上面:选中后它自己变成数字输入框 */}
|
|
443
|
+
{jumpEditing ? (
|
|
444
|
+
<div className="px-1.5 pb-1">
|
|
445
|
+
<input
|
|
446
|
+
autoFocus
|
|
447
|
+
value={jumpValue}
|
|
448
|
+
inputMode="numeric"
|
|
449
|
+
placeholder={t.growthRoundJump}
|
|
450
|
+
title={t.growthRoundJumpHint}
|
|
451
|
+
data-testid="growth-round-input"
|
|
452
|
+
className="w-full rounded-md px-1.5 py-1"
|
|
453
|
+
style={{ fontSize: 12.5, background: "var(--dsw-hover)" }}
|
|
454
|
+
onChange={(e) => setJumpValue(e.target.value)}
|
|
455
|
+
onKeyDown={(e) => {
|
|
456
|
+
if (e.key === "Enter") {
|
|
457
|
+
e.preventDefault();
|
|
458
|
+
jumpToRound(jumpValue);
|
|
459
|
+
}
|
|
460
|
+
}}
|
|
461
|
+
/>
|
|
462
|
+
</div>
|
|
463
|
+
) : (
|
|
464
|
+
<button
|
|
465
|
+
type="button"
|
|
466
|
+
className="flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-left transition-colors"
|
|
467
|
+
style={{ fontSize: 12.5, color: "var(--dsw-label-secondary)" }}
|
|
468
|
+
onMouseEnter={(e) => (e.currentTarget.style.background = "var(--dsw-hover)")}
|
|
469
|
+
onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}
|
|
470
|
+
data-testid="growth-round-jump"
|
|
471
|
+
onClick={() => {
|
|
472
|
+
setJumpValue("");
|
|
473
|
+
setJumpEditing(true);
|
|
474
|
+
}}
|
|
475
|
+
>
|
|
476
|
+
{t.growthRoundJump}
|
|
477
|
+
</button>
|
|
478
|
+
)}
|
|
479
|
+
{/* 轮次列表:当前轮高亮,打开时自动滚到它 */}
|
|
480
|
+
<div ref={roundListRef} className="min-h-0 max-h-[260px] overflow-y-auto border-t pt-1" style={{ borderColor: "var(--dsw-border-l1)" }}>
|
|
481
|
+
{rounds.map((round) => (
|
|
482
|
+
<button
|
|
483
|
+
key={round.commit}
|
|
484
|
+
type="button"
|
|
485
|
+
role="option"
|
|
486
|
+
aria-selected={selected?.commit === round.commit}
|
|
487
|
+
data-selected={selected?.commit === round.commit || undefined}
|
|
488
|
+
className="flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-left transition-colors"
|
|
489
|
+
style={{ background: selected?.commit === round.commit ? "var(--dsw-active)" : "transparent" }}
|
|
490
|
+
onMouseEnter={(e) => {
|
|
491
|
+
if (selected?.commit !== round.commit) e.currentTarget.style.background = "var(--dsw-hover)";
|
|
492
|
+
}}
|
|
493
|
+
onMouseLeave={(e) => {
|
|
494
|
+
if (selected?.commit !== round.commit) e.currentTarget.style.background = "transparent";
|
|
495
|
+
}}
|
|
496
|
+
title={`${roundTitle(round, tt)} · ${fmtTime(round.ts)}`}
|
|
497
|
+
onClick={() => {
|
|
498
|
+
growth.select(round.commit);
|
|
499
|
+
setRoundPickerOpen(false);
|
|
500
|
+
}}
|
|
501
|
+
>
|
|
502
|
+
<span className="min-w-0 flex-1 truncate" style={{ fontSize: 12.5 }}>{roundTitle(round, tt)}</span>
|
|
503
|
+
<span className="flex-none" style={{ fontSize: 11, color: "var(--dsw-label-caption)" }}>{fmtTime(round.ts)}</span>
|
|
504
|
+
</button>
|
|
505
|
+
))}
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
)}
|
|
509
|
+
</div>
|
|
327
510
|
</div>
|
|
328
511
|
</div>
|
|
329
512
|
)}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 设置 → 远程连接(内置插件)。
|
|
5
|
+
*
|
|
6
|
+
* 显示一个内置插件「远程连接」,它有两层入口:
|
|
7
|
+
* - 局域网入口:注入代理,默认就开着;手机连同一个 WiFi 用这个地址,排版是手机版。
|
|
8
|
+
* - 公网入口:cloudflared 快速隧道,由这里的按钮开关;开了之后任何浏览器都能打开。
|
|
9
|
+
*
|
|
10
|
+
* 公开地址由 Cloudflare 随机分配且每次启动都变;拿到地址后还要十几秒才在全球 DNS 生效,
|
|
11
|
+
* 所以状态里区分「已生成」和「已生效」,没生效时按钮区域给出说明而不是假装好了。
|
|
12
|
+
*/
|
|
13
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
14
|
+
import { useI18n } from "@/i18n";
|
|
15
|
+
|
|
16
|
+
interface LanState {
|
|
17
|
+
running: boolean;
|
|
18
|
+
port: number | null;
|
|
19
|
+
urls: string[];
|
|
20
|
+
error: string | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface PublicState {
|
|
24
|
+
running: boolean;
|
|
25
|
+
url: string | null;
|
|
26
|
+
ready: boolean;
|
|
27
|
+
engine: string | null;
|
|
28
|
+
engineAvailable: boolean;
|
|
29
|
+
error: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface RemoteStatus {
|
|
33
|
+
plugin: string;
|
|
34
|
+
targetPort: number;
|
|
35
|
+
/** 非空时服务端拒绝开任何入口(没开认证 / 开发模式) */
|
|
36
|
+
blocked: "auth" | "dev" | null;
|
|
37
|
+
lan: LanState;
|
|
38
|
+
public: PublicState;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 状态点:绿=在跑,蓝=过渡中,灰=没开 */
|
|
42
|
+
function Dot({ tone }: { tone: "on" | "pending" | "off" }) {
|
|
43
|
+
const color = tone === "on" ? "var(--dsw-success)" : tone === "pending" ? "var(--dsw-accent)" : "var(--dsw-label-caption)";
|
|
44
|
+
return <span className="inline-block h-[7px] w-[7px] flex-none rounded-full" style={{ background: color }} />;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function Row({ title, hint, children }: { title: string; hint?: string; children: React.ReactNode }) {
|
|
48
|
+
return (
|
|
49
|
+
<div className="hairline-b flex flex-wrap items-start justify-between gap-4 pb-5">
|
|
50
|
+
<div className="min-w-0 flex-1" style={{ minWidth: "45%" }}>
|
|
51
|
+
<div style={{ fontSize: 14 }}>{title}</div>
|
|
52
|
+
{hint ? <div style={{ fontSize: 12, color: "var(--dsw-label-caption)", lineHeight: 1.55, marginTop: 2 }}>{hint}</div> : null}
|
|
53
|
+
</div>
|
|
54
|
+
<div className="flex-none">{children}</div>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function RemoteSection() {
|
|
60
|
+
const { t } = useI18n();
|
|
61
|
+
const [status, setStatus] = useState<RemoteStatus | null>(null);
|
|
62
|
+
const [busy, setBusy] = useState(false);
|
|
63
|
+
const [error, setError] = useState("");
|
|
64
|
+
const alive = useRef(true);
|
|
65
|
+
|
|
66
|
+
const load = useCallback(async () => {
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetch("/api/remote", { cache: "no-store" });
|
|
69
|
+
const result = await response.json();
|
|
70
|
+
if (!alive.current) return;
|
|
71
|
+
if (result?.success) {
|
|
72
|
+
setStatus(result.data as RemoteStatus);
|
|
73
|
+
setError("");
|
|
74
|
+
} else {
|
|
75
|
+
setError(result?.error ?? t.remoteFailed);
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
if (alive.current) setError(t.remoteFailed);
|
|
79
|
+
}
|
|
80
|
+
}, [t]);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
alive.current = true;
|
|
84
|
+
void load();
|
|
85
|
+
return () => {
|
|
86
|
+
alive.current = false;
|
|
87
|
+
};
|
|
88
|
+
}, [load]);
|
|
89
|
+
|
|
90
|
+
// 地址已生成但还没生效时,短轮询到生效为止(实测这段要十几秒)
|
|
91
|
+
const waiting = Boolean(status?.public.running && !status.public.ready);
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (!waiting) return;
|
|
94
|
+
const timer = setInterval(() => void load(), 2_500);
|
|
95
|
+
return () => clearInterval(timer);
|
|
96
|
+
}, [waiting, load]);
|
|
97
|
+
|
|
98
|
+
const act = async (action: "start" | "stop") => {
|
|
99
|
+
if (busy) return;
|
|
100
|
+
setBusy(true);
|
|
101
|
+
setError("");
|
|
102
|
+
try {
|
|
103
|
+
const response = await fetch("/api/remote", {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: { "Content-Type": "application/json" },
|
|
106
|
+
body: JSON.stringify({ action }),
|
|
107
|
+
});
|
|
108
|
+
const result = await response.json();
|
|
109
|
+
if (result?.success) setStatus(result.data as RemoteStatus);
|
|
110
|
+
else setError(result?.error ?? t.remoteFailed);
|
|
111
|
+
} catch {
|
|
112
|
+
setError(t.remoteFailed);
|
|
113
|
+
} finally {
|
|
114
|
+
if (alive.current) setBusy(false);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// 「让所有设备重新登录」点两下才生效(第一下只把按钮换成确认文案,4 秒后复原)
|
|
119
|
+
const [confirmSignOut, setConfirmSignOut] = useState(false);
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!confirmSignOut) return;
|
|
122
|
+
const timer = setTimeout(() => setConfirmSignOut(false), 4_000);
|
|
123
|
+
return () => clearTimeout(timer);
|
|
124
|
+
}, [confirmSignOut]);
|
|
125
|
+
const signOutAll = async () => {
|
|
126
|
+
if (!confirmSignOut) {
|
|
127
|
+
setConfirmSignOut(true);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
setConfirmSignOut(false);
|
|
131
|
+
setError("");
|
|
132
|
+
try {
|
|
133
|
+
const response = await fetch("/api/web-auth?scope=all", { method: "DELETE" });
|
|
134
|
+
const result = await response.json().catch(() => null);
|
|
135
|
+
if (!response.ok || !result?.success) throw new Error(result?.error ?? t.remoteFailed);
|
|
136
|
+
window.location.href = "/login";
|
|
137
|
+
} catch (e) {
|
|
138
|
+
if (alive.current) setError(e instanceof Error ? e.message : t.remoteFailed);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const lan = status?.lan;
|
|
143
|
+
const pub = status?.public;
|
|
144
|
+
const blocked = status?.blocked ?? null;
|
|
145
|
+
const lanTone: "on" | "off" = lan?.running ? "on" : "off";
|
|
146
|
+
const pubTone: "on" | "pending" | "off" = pub?.running ? (pub.ready ? "on" : "pending") : "off";
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<div className="flex flex-col gap-6">
|
|
150
|
+
{/* 插件抬头 */}
|
|
151
|
+
<div className="flex items-center gap-2.5">
|
|
152
|
+
<div style={{ fontSize: 15, fontWeight: 500 }}>{t.setRemote}</div>
|
|
153
|
+
<span
|
|
154
|
+
className="rounded-md px-2 py-[2px]"
|
|
155
|
+
style={{ fontSize: 11, background: "var(--dsw-hover)", color: "var(--dsw-label-secondary)" }}
|
|
156
|
+
>
|
|
157
|
+
{t.remoteBuiltin}
|
|
158
|
+
</span>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{/* 局域网入口 */}
|
|
162
|
+
<Row title={t.remoteLanTitle} hint={t.remoteLanHint}>
|
|
163
|
+
<div className="flex flex-col items-end gap-1.5">
|
|
164
|
+
<div className="flex items-center gap-2" style={{ fontSize: 13 }}>
|
|
165
|
+
<Dot tone={lanTone} />
|
|
166
|
+
<span style={{ color: lan?.running ? "var(--dsw-label-primary)" : "var(--dsw-label-caption)" }}>
|
|
167
|
+
{lan?.running ? t.remoteRunning : t.remoteStopped}
|
|
168
|
+
</span>
|
|
169
|
+
</div>
|
|
170
|
+
{(lan?.urls ?? []).map((url) => (
|
|
171
|
+
<a key={url} href={url} target="_blank" rel="noreferrer" className="font-mono hover:underline" style={{ fontSize: 12 }}>
|
|
172
|
+
{url}
|
|
173
|
+
</a>
|
|
174
|
+
))}
|
|
175
|
+
{lan?.error ? <div style={{ fontSize: 12, color: "var(--dsw-danger)" }}>{lan.error}</div> : null}
|
|
176
|
+
</div>
|
|
177
|
+
</Row>
|
|
178
|
+
|
|
179
|
+
{/* 公网入口 */}
|
|
180
|
+
<Row title={t.remotePublicTitle} hint={t.remotePublicHint}>
|
|
181
|
+
<div className="flex flex-col items-end gap-2">
|
|
182
|
+
<div className="flex items-center gap-2" style={{ fontSize: 13 }}>
|
|
183
|
+
<Dot tone={pubTone} />
|
|
184
|
+
<span style={{ color: pub?.running ? "var(--dsw-label-primary)" : "var(--dsw-label-caption)" }}>
|
|
185
|
+
{pub?.running ? (pub.ready ? t.remoteRunning : t.remotePending) : t.remoteStopped}
|
|
186
|
+
</span>
|
|
187
|
+
</div>
|
|
188
|
+
{pub?.url ? (
|
|
189
|
+
<a href={pub.url} target="_blank" rel="noreferrer" className="font-mono hover:underline" style={{ fontSize: 12 }}>
|
|
190
|
+
{pub.url}
|
|
191
|
+
</a>
|
|
192
|
+
) : null}
|
|
193
|
+
<div className="flex items-center gap-2">
|
|
194
|
+
{pub?.running ? (
|
|
195
|
+
<button className="btn-outline" style={{ height: 30, fontSize: 13 }} disabled={busy} onClick={() => void act("stop")}>
|
|
196
|
+
{t.remoteStop}
|
|
197
|
+
</button>
|
|
198
|
+
) : (
|
|
199
|
+
<button className="btn-primary-white" style={{ height: 30, fontSize: 13 }} disabled={busy || blocked !== null} onClick={() => void act("start")}>
|
|
200
|
+
{busy ? t.remoteWorking : t.remoteStart}
|
|
201
|
+
</button>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</Row>
|
|
206
|
+
|
|
207
|
+
{/* 没开认证就没有登录这回事 */}
|
|
208
|
+
{status && blocked !== "auth" ? (
|
|
209
|
+
<Row title={t.remoteSessionsTitle} hint={t.remoteSessionsHint}>
|
|
210
|
+
<button
|
|
211
|
+
className="btn-outline"
|
|
212
|
+
style={{ height: 30, fontSize: 13, color: confirmSignOut ? "var(--dsw-danger)" : undefined }}
|
|
213
|
+
onClick={() => void signOutAll()}
|
|
214
|
+
>
|
|
215
|
+
{confirmSignOut ? t.remoteSignOutAllConfirm : t.remoteSignOutAll}
|
|
216
|
+
</button>
|
|
217
|
+
</Row>
|
|
218
|
+
) : null}
|
|
219
|
+
|
|
220
|
+
{blocked ? (
|
|
221
|
+
<div style={{ fontSize: 12, color: "var(--dsw-label-secondary)", lineHeight: 1.6 }}>
|
|
222
|
+
{blocked === "auth" ? t.remoteBlockedAuth : t.remoteBlockedDev}
|
|
223
|
+
</div>
|
|
224
|
+
) : null}
|
|
225
|
+
{pub && !pub.engineAvailable ? (
|
|
226
|
+
<div style={{ fontSize: 12, color: "var(--dsw-danger)", lineHeight: 1.6 }}>{t.remoteEngineMissing}</div>
|
|
227
|
+
) : null}
|
|
228
|
+
{pub?.error ? <div style={{ fontSize: 12, color: "var(--dsw-danger)", lineHeight: 1.6 }}>{pub.error}</div> : null}
|
|
229
|
+
{error ? <div role="alert" style={{ fontSize: 12, color: "var(--dsw-danger)" }}>{error}</div> : null}
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
@@ -200,50 +200,38 @@ export function SessionImportSection({ cwd }: { cwd: string }) {
|
|
|
200
200
|
const skipped = report?.results.filter((result) => result.status === "skipped") ?? [];
|
|
201
201
|
|
|
202
202
|
return (
|
|
203
|
-
<div className="flex flex-col gap-
|
|
204
|
-
|
|
205
|
-
<div
|
|
206
|
-
{
|
|
203
|
+
<div className="flex flex-col gap-3.5" data-testid="session-import">
|
|
204
|
+
{/* 说明区:主说明 + 扫描范围提示收进一个卡片,和下面的操作区分开 */}
|
|
205
|
+
<div className="flex flex-col gap-1.5 rounded-2xl px-4 py-3" style={{ background: "var(--dsw-hover)" }}>
|
|
206
|
+
<div style={{ fontSize: 12.5, color: "var(--dsw-label-secondary)", lineHeight: 1.6 }}>{t.importDesc}</div>
|
|
207
|
+
<div style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }} title={t.importLimitHintTooltip} data-testid="import-limit-hint">
|
|
208
|
+
{t.importLimitHint.replace("{n}", String(SCAN_LIMIT))}
|
|
209
|
+
</div>
|
|
207
210
|
</div>
|
|
208
211
|
|
|
209
|
-
{/*
|
|
212
|
+
{/* 操作行:扫描 / 清空 / 导入。去向是"设置",单独一行读起来更清楚 */}
|
|
210
213
|
<div className="flex flex-wrap items-center gap-2">
|
|
211
214
|
<button
|
|
212
|
-
className="flex items-center gap-1.5
|
|
213
|
-
style={{
|
|
215
|
+
className="btn-outline flex flex-none items-center gap-1.5 whitespace-nowrap"
|
|
216
|
+
style={{ height: 30, padding: "0 12px", fontSize: 12.5 }}
|
|
214
217
|
onClick={() => void scan()}
|
|
215
218
|
disabled={scanning}
|
|
216
219
|
data-testid="import-rescan"
|
|
217
220
|
>
|
|
218
221
|
<IconRefreshOutline14 size={13} /> {scanning ? t.importScanning : t.importRescan}
|
|
219
222
|
</button>
|
|
220
|
-
<span style={{ fontSize: 12, color: "var(--dsw-label-caption)" }}>{t.importFallbackWorkspace}</span>
|
|
221
|
-
<select
|
|
222
|
-
className="rounded-lg px-2 py-1"
|
|
223
|
-
style={{ fontSize: 12, border: "0.5px solid var(--dsw-border-l2)", background: "transparent", maxWidth: 260 }}
|
|
224
|
-
value={fallback}
|
|
225
|
-
onChange={(event) => setFallback(event.target.value)}
|
|
226
|
-
aria-label={t.importFallbackWorkspace}
|
|
227
|
-
data-testid="import-fallback"
|
|
228
|
-
>
|
|
229
|
-
{(workspaces.includes(fallback) || !fallback ? workspaces : [fallback, ...workspaces]).map((workspace) => (
|
|
230
|
-
<option key={workspace} value={workspace}>
|
|
231
|
-
{workspace}
|
|
232
|
-
</option>
|
|
233
|
-
))}
|
|
234
|
-
</select>
|
|
235
223
|
<div className="flex-1" />
|
|
236
224
|
<button
|
|
237
|
-
className="rounded-lg px-2.5 py-1.5"
|
|
238
|
-
style={{ fontSize: 12, color: "var(--dsw-label-tertiary)" }}
|
|
225
|
+
className="whitespace-nowrap rounded-lg px-2.5 py-1.5"
|
|
226
|
+
style={{ fontSize: 12, color: selected.size ? "var(--dsw-label-secondary)" : "var(--dsw-label-tertiary)" }}
|
|
239
227
|
onClick={() => setSelected(new Set())}
|
|
240
228
|
disabled={!selected.size}
|
|
241
229
|
>
|
|
242
230
|
{t.importClearSelection}
|
|
243
231
|
</button>
|
|
244
232
|
<button
|
|
245
|
-
className="flex items-center gap-1.5 rounded-lg px-3 py-1.5"
|
|
246
|
-
style={{ fontSize: 12, background: selected.size && !importing ? "var(--dsw-accent)" : "var(--dsw-hover)", color: selected.size && !importing ? "#fff" : "var(--dsw-label-caption)" }}
|
|
233
|
+
className="flex flex-none items-center gap-1.5 whitespace-nowrap rounded-lg px-3 py-1.5"
|
|
234
|
+
style={{ fontSize: 12.5, background: selected.size && !importing ? "var(--dsw-accent)" : "var(--dsw-hover)", color: selected.size && !importing ? "#fff" : "var(--dsw-label-caption)" }}
|
|
247
235
|
onClick={() => void run()}
|
|
248
236
|
disabled={!selected.size || importing}
|
|
249
237
|
data-testid="import-run"
|
|
@@ -251,6 +239,24 @@ export function SessionImportSection({ cwd }: { cwd: string }) {
|
|
|
251
239
|
<IconDownloadOutline16 size={13} /> {importing ? t.importRunning : t.importRun.replace("{n}", String(selected.size))}
|
|
252
240
|
</button>
|
|
253
241
|
</div>
|
|
242
|
+
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
|
243
|
+
<span className="flex-none" style={{ fontSize: 12, color: "var(--dsw-label-caption)" }}>{t.importFallbackWorkspace}</span>
|
|
244
|
+
<select
|
|
245
|
+
className="min-w-0 rounded-lg px-2 py-1"
|
|
246
|
+
style={{ fontSize: 12, border: "0.5px solid var(--dsw-border-l2)", background: "transparent", maxWidth: 280 }}
|
|
247
|
+
value={fallback}
|
|
248
|
+
onChange={(event) => setFallback(event.target.value)}
|
|
249
|
+
aria-label={t.importFallbackWorkspace}
|
|
250
|
+
data-testid="import-fallback"
|
|
251
|
+
>
|
|
252
|
+
{(workspaces.includes(fallback) || !fallback ? workspaces : [fallback, ...workspaces]).map((workspace) => (
|
|
253
|
+
<option key={workspace} value={workspace}>
|
|
254
|
+
{workspace}
|
|
255
|
+
</option>
|
|
256
|
+
))}
|
|
257
|
+
</select>
|
|
258
|
+
<span className="min-w-0" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>{t.importFallbackHint}</span>
|
|
259
|
+
</div>
|
|
254
260
|
|
|
255
261
|
{failure && (
|
|
256
262
|
<div role="alert" className="rounded-lg px-3 py-2" style={{ fontSize: 12.5, background: "var(--dsw-hover)", color: "var(--dsw-danger)" }}>
|
|
@@ -335,30 +341,46 @@ export function SessionImportSection({ cwd }: { cwd: string }) {
|
|
|
335
341
|
const rowKey = key(row);
|
|
336
342
|
const done = importedKeys.has(rowKey);
|
|
337
343
|
const checked = selected.has(rowKey);
|
|
344
|
+
// 去向:源项目目录还在本机就回原工作区(服务端判定),否则用兜底工作区。
|
|
345
|
+
// 这里只按"有没有 projectPath"给出预期,不假装知道目录是否真的存在。
|
|
346
|
+
const target = row.projectPath || fallback;
|
|
347
|
+
const goesOriginal = Boolean(row.projectPath);
|
|
338
348
|
return (
|
|
339
349
|
<label
|
|
340
350
|
key={rowKey}
|
|
341
|
-
className="flex cursor-pointer items-center gap-3 rounded-xl px-3 py-2"
|
|
342
|
-
style={{
|
|
351
|
+
className="flex cursor-pointer items-center gap-3 rounded-xl px-3 py-2 transition-colors"
|
|
352
|
+
style={{
|
|
353
|
+
border: `0.5px solid ${checked ? "var(--dsw-accent)" : "var(--dsw-border-l2)"}`,
|
|
354
|
+
background: checked ? "var(--dsw-accent-soft)" : "transparent",
|
|
355
|
+
opacity: done ? 0.5 : 1,
|
|
356
|
+
}}
|
|
343
357
|
>
|
|
344
|
-
<input type="checkbox" checked={checked} disabled={done} onChange={() => toggle(row)} />
|
|
345
|
-
<div className="min-w-0 flex-1">
|
|
346
|
-
<div className="truncate" style={{ fontSize: 12.5 }} title={row.title}>
|
|
358
|
+
<input type="checkbox" checked={checked} disabled={done} onChange={() => toggle(row)} className="flex-none" />
|
|
359
|
+
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
360
|
+
<div className="truncate" style={{ fontSize: 12.5, color: done ? "var(--dsw-label-tertiary)" : "var(--dsw-label-primary)" }} title={row.title}>
|
|
347
361
|
{row.title || t.importUntitled}
|
|
348
362
|
</div>
|
|
349
|
-
<div className="
|
|
350
|
-
|
|
363
|
+
<div className="flex min-w-0 items-center gap-1.5" title={row.projectPath}>
|
|
364
|
+
<span
|
|
365
|
+
className="flex-none rounded px-1 py-0.5"
|
|
366
|
+
style={{ fontSize: 10, background: "var(--dsw-selector)", color: "var(--dsw-label-tertiary)" }}
|
|
367
|
+
>
|
|
368
|
+
{goesOriginal ? t.importGoesOriginal : t.importGoesFallback}
|
|
369
|
+
</span>
|
|
370
|
+
<span className="truncate" style={{ fontSize: 11, color: "var(--dsw-label-caption)" }}>
|
|
371
|
+
{target || t.importNoProject}
|
|
372
|
+
</span>
|
|
351
373
|
</div>
|
|
352
374
|
</div>
|
|
375
|
+
<span className="flex-none" style={{ fontSize: 11, color: "var(--dsw-label-caption)", whiteSpace: "nowrap" }}>
|
|
376
|
+
{typeof row.messageCount === "number" ? t.importMessages.replace("{n}", String(row.messageCount)) : "—"}
|
|
377
|
+
</span>
|
|
378
|
+
<span className="flex-none" style={{ fontSize: 11, color: "var(--dsw-label-caption)", whiteSpace: "nowrap" }}>{dayOf(row.updatedAt ?? row.createdAt)}</span>
|
|
353
379
|
{done && (
|
|
354
|
-
<span className="rounded-full px-2 py-0.5" style={{ fontSize: 10, background: "var(--dsw-
|
|
380
|
+
<span className="flex-none rounded-full px-2 py-0.5" style={{ fontSize: 10, background: "var(--dsw-selector)", color: "var(--dsw-label-caption)" }}>
|
|
355
381
|
{t.importAlready}
|
|
356
382
|
</span>
|
|
357
383
|
)}
|
|
358
|
-
<span style={{ fontSize: 11, color: "var(--dsw-label-caption)", whiteSpace: "nowrap" }}>
|
|
359
|
-
{typeof row.messageCount === "number" ? t.importMessages.replace("{n}", String(row.messageCount)) : "—"}
|
|
360
|
-
</span>
|
|
361
|
-
<span style={{ fontSize: 11, color: "var(--dsw-label-caption)", whiteSpace: "nowrap" }}>{dayOf(row.updatedAt ?? row.createdAt)}</span>
|
|
362
384
|
</label>
|
|
363
385
|
);
|
|
364
386
|
})}
|