@rexvane/piweb 0.3.24 → 0.3.26
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 +11 -11
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +5 -5
- package/.next/react-loadable-manifest.json +13 -20
- package/.next/required-server-files.js +2 -1
- package/.next/required-server-files.json +2 -1
- package/.next/routes-manifest.json +6 -8
- 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 +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/agent/resources/route.js +1 -0
- package/.next/server/app/api/agent/resources/route.js.nft.json +1 -0
- package/.next/server/app/api/agent/resources/route_client-reference-manifest.js +1 -0
- 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 -1
- package/.next/server/app/api/fs/route.js.nft.json +1 -1
- package/.next/server/app/api/git/route.js +4 -4
- package/.next/server/app/api/git/route.js.nft.json +1 -1
- package/.next/server/app/api/github/route.js +4 -4
- package/.next/server/app/api/github/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/defaults/route.js +1 -1
- package/.next/server/app/api/models/defaults/route.js.nft.json +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 +4 -3
- 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 +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 +2 -2
- 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 -7
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +6 -7
- package/.next/server/app/index.segments/_full.segment.rsc +6 -7
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -3
- 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 +11 -11
- package/.next/server/chunks/3924.js +3 -3
- package/.next/server/chunks/538.js +1 -1
- package/.next/server/chunks/620.js +1 -0
- package/.next/server/chunks/6229.js +1 -1
- package/.next/server/chunks/7496.js +1 -1
- package/.next/server/chunks/7662.js +2 -2
- package/.next/server/chunks/8686.js +1 -1
- package/.next/server/functions-config-manifest.json +1 -2
- package/.next/server/instrumentation.js.nft.json +1 -1
- 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/preview-props.json +3 -3
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/QJCtqgDKzGS2BdKABd9ch/_buildManifest.js +1 -0
- package/.next/static/chunks/2577.86c8538fbf05a8b2.js +1 -0
- package/.next/static/chunks/2716.fa561519675a3ff1.js +1 -0
- package/.next/static/chunks/2906.8a0e09bb47605cf1.js +1 -0
- package/.next/static/chunks/4673.ea36e55005c0f82f.js +1 -0
- package/.next/static/chunks/4686-4dc565817a160351.js +1 -0
- package/.next/static/chunks/4778.0d2c5d75b791d352.js +1 -0
- package/.next/static/chunks/{618.949981da99be7182.js → 618.9a22656b863f242b.js} +2 -2
- package/.next/static/chunks/app/_global-error/page-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/events/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/tree/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/new/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/resources/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/config/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/files/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/fs/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/git/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/github/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/growth/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/health/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/models/defaults/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/models/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/pi-settings/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/plugins/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/prompts/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/remote/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/security/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/export/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/sessions/import/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/sessions/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/skills/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/update/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/version/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/web-auth/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/workspaces/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/page-71f2d3b3ed864b3d.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/webpack-ffc09b5fcc8aaa6d.js +1 -0
- package/.next/static/css/014eb1f21414e615.css +1 -0
- package/.next/static/css/56b129a13637fcfd.css +1 -0
- package/.next/static/css/7e3caf797e4cb19a.css +1 -0
- package/.next/types/app/api/agent/{[id]/side-chat → resources}/route.ts +3 -3
- package/.next/types/routes.d.ts +2 -2
- package/.next/types/validator.ts +9 -9
- package/README.md +1 -1
- package/next.config.ts +1 -1
- package/package.json +9 -9
- package/scripts/apply-global.mjs +87 -0
- package/scripts/launcher.mjs +21 -0
- package/scripts/testing/prepare-sdk-tools.mjs +19 -0
- package/src/app/api/agent/new/route.ts +2 -1
- package/src/app/api/agent/resources/route.ts +18 -0
- package/src/app/api/models/route.ts +22 -1
- package/src/app/api/plugins/route.ts +3 -2
- package/src/app/api/prompts/route.ts +3 -2
- package/src/app/api/remote/route.ts +22 -4
- package/src/app/api/sessions/[id]/export/route.ts +3 -1
- package/src/app/api/skills/route.ts +3 -2
- package/src/app/globals.css +1002 -1765
- package/src/components/AppShell.tsx +128 -93
- package/src/components/ChatInput.tsx +177 -98
- package/src/components/ChatWindow.tsx +189 -56
- package/src/components/ContextMeter.tsx +33 -9
- package/src/components/CustomSelect.module.css +142 -0
- package/src/components/CustomSelect.tsx +157 -0
- package/src/components/DrawerPanel.tsx +1 -1
- package/src/components/ExtensionUI.tsx +9 -3
- package/src/components/FileViewer.tsx +24 -21
- package/src/components/GitPanel.tsx +35 -13
- package/src/components/GrowthTree.tsx +6 -2
- package/src/components/ImageViewer.tsx +72 -0
- package/src/components/MarkdownBlock.tsx +67 -96
- package/src/components/ModelSelector.tsx +36 -39
- package/src/components/PiMark.tsx +76 -41
- package/src/components/PreviewImage.tsx +33 -0
- package/src/components/ProjectPanel.tsx +8 -5
- package/src/components/PromptPanel.tsx +85 -20
- package/src/components/ProviderSetupModal.module.css +12 -11
- package/src/components/ProviderSetupModal.tsx +107 -36
- package/src/components/RemoteSection.tsx +53 -2
- package/src/components/SessionImportSection.tsx +30 -15
- package/src/components/SessionSidebar.tsx +72 -62
- package/src/components/SettingsPanel.tsx +49 -151
- package/src/components/TrajectoryAnalyzer.module.css +26 -11
- package/src/components/TrajectoryView.tsx +11 -3
- package/src/components/chat/markdown-actions.tsx +23 -0
- package/src/components/composer/ComposerFiles.tsx +224 -0
- package/src/components/composer/ComposerImages.tsx +59 -127
- package/src/hooks/usePiWeb.ts +47 -2
- package/src/i18n.tsx +62 -80
- package/src/lib/agent-manager.ts +365 -33
- package/src/lib/chat/file-path.ts +54 -0
- package/src/lib/chat/skill-expansion.ts +51 -0
- package/src/lib/extension-ui.ts +25 -1
- package/src/lib/font.ts +37 -39
- package/src/lib/git-service.ts +4 -2
- package/src/lib/growth-turns.ts +3 -1
- package/src/lib/model-draft.ts +69 -2
- package/src/lib/models-service.ts +53 -3
- package/src/lib/pi.ts +2 -2
- package/src/lib/remote/assets.json +1 -1
- package/src/lib/remote/supervisor.ts +48 -5
- package/src/lib/security-service.ts +2 -1
- package/src/lib/session-reader.ts +25 -13
- package/src/lib/session-retention.ts +11 -8
- package/src/lib/skills-service.ts +2 -1
- package/src/lib/slash-command-token.ts +14 -0
- package/src/lib/trajectory.ts +111 -15
- package/src/lib/types.ts +28 -3
- package/src/lib/update-service.ts +29 -9
- package/src/lib/workspace-store.ts +36 -1
- package/.next/server/app/api/agent/[id]/side-chat/route.js +0 -5
- package/.next/server/app/api/agent/[id]/side-chat/route.js.nft.json +0 -1
- package/.next/server/app/api/agent/[id]/side-chat/route_client-reference-manifest.js +0 -1
- package/.next/server/chunks/3421.js +0 -1
- package/.next/static/chunks/2716.44fdf1b21454b961.js +0 -1
- package/.next/static/chunks/4673.e261707dce391be7.js +0 -1
- package/.next/static/chunks/4686-0cd87513f9ea3538.js +0 -1
- package/.next/static/chunks/4778.eddd3a5813f85824.js +0 -1
- package/.next/static/chunks/7411.5f79671208209b13.js +0 -1
- package/.next/static/chunks/8552.25c39ef4c995eba0.js +0 -1
- package/.next/static/chunks/app/_global-error/page-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/events/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/side-chat/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/tree/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/new/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/config/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/files/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/fs/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/git/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/github/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/growth/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/health/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/models/defaults/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/models/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/pi-settings/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/plugins/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/prompts/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/remote/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/security/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/export/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/sessions/import/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/skills/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/update/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/version/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/web-auth/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/workspaces/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/page-dc3cb41d91c7432e.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/webpack-2ac3d76b81895f5b.js +0 -1
- package/.next/static/css/08815507594e8b8d.css +0 -1
- package/.next/static/css/9fc559c2c4ad1d0f.css +0 -1
- package/.next/static/css/c6e2227c36ec2fd6.css +0 -1
- package/.next/static/css/f15a853af8b5127f.css +0 -1
- package/.next/static/lvKCJmjFj8LuqSojRVLRF/_buildManifest.js +0 -1
- package/src/app/api/agent/[id]/side-chat/route.ts +0 -77
- package/src/components/side-chat/SideChatPanel.module.css +0 -131
- package/src/components/side-chat/SideChatPanel.tsx +0 -189
- package/src/components/side-chat/icons.tsx +0 -8
- package/src/components/side-chat/request-history.ts +0 -29
- package/src/components/side-chat/store.ts +0 -248
- package/src/lib/chat/stream-settings.ts +0 -192
- package/src/lib/side-chat/context.ts +0 -123
- package/src/lib/side-chat/service.ts +0 -191
- package/src/lib/side-chat/types.ts +0 -61
- /package/.next/static/{lvKCJmjFj8LuqSojRVLRF → QJCtqgDKzGS2BdKABd9ch}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 输入卡里上传的文件列表(多文件牌堆与竖直向上弧度展开):
|
|
5
|
+
* - 不超过 COLLAPSE_AT 个:平铺文件 chip。
|
|
6
|
+
* - 超过时折叠成牌堆:第一张在最上,后面露 4 层边,显示「展开 N」;
|
|
7
|
+
* - 点「展开」沿竖直向上呈优美弧度扇开,每张都可以单独删除或悬停预览,
|
|
8
|
+
* 点击「收起」平滑收回牌堆;与图片牌堆/扇形拥有一致的动效与交互质感。
|
|
9
|
+
*/
|
|
10
|
+
import { useEffect, useRef, useState } from "react";
|
|
11
|
+
import { IconFileOutline16 } from "@/components/icons";
|
|
12
|
+
import { useI18n } from "@/i18n";
|
|
13
|
+
import { fmtBytes } from "@/lib/format";
|
|
14
|
+
|
|
15
|
+
export const COLLAPSE_AT = 4;
|
|
16
|
+
const FILE_CARD_W = 196;
|
|
17
|
+
const FILE_CARD_H = 32;
|
|
18
|
+
const DECK_PEEK = 4;
|
|
19
|
+
const DECK_LAYERS = 4;
|
|
20
|
+
const FAN_SIDE = 6;
|
|
21
|
+
|
|
22
|
+
export interface FileCardPose {
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
angle: number;
|
|
26
|
+
hidden?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface FileLayout {
|
|
30
|
+
cards: FileCardPose[];
|
|
31
|
+
width: number;
|
|
32
|
+
height: number;
|
|
33
|
+
gap: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const round = (value: number) => Math.round(value * 10) / 10;
|
|
37
|
+
const fmtUploadSize = (bytes?: number) => (bytes ? fmtBytes(bytes) : "");
|
|
38
|
+
|
|
39
|
+
export function fileDeckLayout(count: number): FileLayout {
|
|
40
|
+
const cards = Array.from({ length: count }, (_, i) => ({
|
|
41
|
+
x: Math.min(i, DECK_LAYERS) * DECK_PEEK,
|
|
42
|
+
y: 0,
|
|
43
|
+
angle: 0,
|
|
44
|
+
hidden: i >= DECK_LAYERS,
|
|
45
|
+
}));
|
|
46
|
+
return {
|
|
47
|
+
cards,
|
|
48
|
+
width: FILE_CARD_W + Math.min(Math.max(count - 1, 0), DECK_LAYERS) * DECK_PEEK,
|
|
49
|
+
height: FILE_CARD_H,
|
|
50
|
+
gap: DECK_PEEK,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function fileFanLayout(count: number, width: number): FileLayout {
|
|
55
|
+
if (count <= 1) {
|
|
56
|
+
return { cards: [{ x: FAN_SIDE, y: 0, angle: 0 }], width: FILE_CARD_W + FAN_SIDE * 2, height: FILE_CARD_H, gap: 0 };
|
|
57
|
+
}
|
|
58
|
+
// 垂直每张抬升步长:自适应压窄,保证上方不顶出视口
|
|
59
|
+
const stepY = Math.min(32, Math.max(16, 220 / (count - 1)));
|
|
60
|
+
const spanY = (count - 1) * stepY;
|
|
61
|
+
const totalH = FILE_CARD_H + spanY + 14;
|
|
62
|
+
const cards = Array.from({ length: count }, (_, i) => {
|
|
63
|
+
const progress = i / (count - 1);
|
|
64
|
+
// y 轴:第 0 张在底部,最后一张在最上方
|
|
65
|
+
const y = round((totalH - FILE_CARD_H - 6) - i * stepY);
|
|
66
|
+
// x 轴:向上升起时呈优雅的弧度向右轻微弯出
|
|
67
|
+
const arcX = 18 * Math.sin(progress * Math.PI);
|
|
68
|
+
const x = round(FAN_SIDE + arcX + progress * 10);
|
|
69
|
+
// 角度:从下方的 -3° 顺滑过渡到上方的 +5°
|
|
70
|
+
const angle = round(-3 + progress * 8);
|
|
71
|
+
return { x, y, angle };
|
|
72
|
+
});
|
|
73
|
+
const maxX = Math.max(...cards.map((c) => c.x)) + FILE_CARD_W + FAN_SIDE;
|
|
74
|
+
return {
|
|
75
|
+
cards,
|
|
76
|
+
width: Math.min(width, Math.max(FILE_CARD_W + 32, Math.ceil(maxX))),
|
|
77
|
+
height: Math.ceil(totalH),
|
|
78
|
+
gap: round(stepY),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface UploadFileItem {
|
|
83
|
+
name: string;
|
|
84
|
+
path: string;
|
|
85
|
+
size?: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function ComposerFiles({
|
|
89
|
+
files,
|
|
90
|
+
disabled,
|
|
91
|
+
onRemove,
|
|
92
|
+
onClear,
|
|
93
|
+
}: {
|
|
94
|
+
files: UploadFileItem[];
|
|
95
|
+
disabled?: boolean;
|
|
96
|
+
onRemove: (index: number) => void;
|
|
97
|
+
onClear?: () => void;
|
|
98
|
+
}) {
|
|
99
|
+
const { t } = useI18n();
|
|
100
|
+
const collapsible = files.length > COLLAPSE_AT;
|
|
101
|
+
const [expanded, setExpanded] = useState(false);
|
|
102
|
+
const fanned = collapsible && expanded;
|
|
103
|
+
const [lifted, setLifted] = useState<number | null>(null);
|
|
104
|
+
const railRef = useRef<HTMLDivElement>(null);
|
|
105
|
+
const [railWidth, setRailWidth] = useState(360);
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const rail = railRef.current;
|
|
109
|
+
if (!rail || !collapsible) return;
|
|
110
|
+
const measure = () => setRailWidth(Math.max(280, rail.clientWidth - 8));
|
|
111
|
+
measure();
|
|
112
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
113
|
+
const observer = new ResizeObserver(measure);
|
|
114
|
+
observer.observe(rail);
|
|
115
|
+
return () => observer.disconnect();
|
|
116
|
+
}, [collapsible]);
|
|
117
|
+
|
|
118
|
+
const layout = fanned ? fileFanLayout(files.length, railWidth) : fileDeckLayout(files.length);
|
|
119
|
+
|
|
120
|
+
const renderFileCard = (f: UploadFileItem, i: number, pose?: FileCardPose, isDeck = false) => {
|
|
121
|
+
const up = fanned && lifted === i;
|
|
122
|
+
const delay = Math.min(fanned ? i * 14 : (files.length - 1 - i) * 10, 140);
|
|
123
|
+
const isHidden = !fanned && isDeck && i >= DECK_LAYERS;
|
|
124
|
+
if (isHidden) return null;
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div
|
|
128
|
+
key={`${f.path}:${i}`}
|
|
129
|
+
className="pw-ref-chip group/ref pw-file-card"
|
|
130
|
+
title={f.path}
|
|
131
|
+
data-lifted={up || undefined}
|
|
132
|
+
style={
|
|
133
|
+
pose
|
|
134
|
+
? {
|
|
135
|
+
position: "absolute",
|
|
136
|
+
left: 0,
|
|
137
|
+
top: 0,
|
|
138
|
+
width: FILE_CARD_W,
|
|
139
|
+
height: FILE_CARD_H,
|
|
140
|
+
zIndex: up ? files.length + 2 : fanned ? i + 1 : files.length - i,
|
|
141
|
+
transform: `translate(${pose.x}px, ${pose.y - (up ? 4 : 0)}px) rotate(${pose.angle}deg) ${up ? "scale(1.04)" : ""}`,
|
|
142
|
+
transitionDelay: up || lifted !== null ? "0ms" : `${delay}ms`,
|
|
143
|
+
}
|
|
144
|
+
: undefined
|
|
145
|
+
}
|
|
146
|
+
onMouseEnter={() => fanned && setLifted(i)}
|
|
147
|
+
onMouseLeave={() => setLifted((cur) => (cur === i ? null : cur))}
|
|
148
|
+
onFocus={() => fanned && setLifted(i)}
|
|
149
|
+
onBlur={(e) => {
|
|
150
|
+
if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setLifted((cur) => (cur === i ? null : cur));
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
<IconFileOutline16 size={12} style={{ flex: "none", color: "var(--dsw-label-tertiary)" }} />
|
|
154
|
+
<span className="max-w-[110px] truncate font-mono text-[12px]">{f.name}</span>
|
|
155
|
+
<span style={{ flex: "none", fontSize: 10.5, color: "var(--dsw-label-caption)" }}>{fmtUploadSize(f.size)}</span>
|
|
156
|
+
<button
|
|
157
|
+
type="button"
|
|
158
|
+
className="pw-ref-close"
|
|
159
|
+
disabled={disabled}
|
|
160
|
+
onClick={(e) => {
|
|
161
|
+
e.stopPropagation();
|
|
162
|
+
onRemove(i);
|
|
163
|
+
setLifted(null);
|
|
164
|
+
}}
|
|
165
|
+
aria-label={`${t.delete} ${f.name}`}
|
|
166
|
+
>
|
|
167
|
+
✕
|
|
168
|
+
</button>
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<div ref={railRef} className="pointer-events-auto flex flex-wrap items-end gap-2 px-1 pb-1" data-testid="composer-uploads">
|
|
175
|
+
{collapsible ? (
|
|
176
|
+
<div
|
|
177
|
+
className="pw-file-fan-container relative flex items-center gap-2"
|
|
178
|
+
style={{
|
|
179
|
+
position: "relative",
|
|
180
|
+
width: fanned ? layout.width : layout.width + 8,
|
|
181
|
+
height: layout.height,
|
|
182
|
+
transition: "width 320ms cubic-bezier(0.2, 0.8, 0.2, 1), height 320ms cubic-bezier(0.2, 0.8, 0.2, 1)",
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
{files.map((f, i) => renderFileCard(f, i, layout.cards[i], true))}
|
|
186
|
+
</div>
|
|
187
|
+
) : (
|
|
188
|
+
files.map((f, i) => renderFileCard(f, i))
|
|
189
|
+
)}
|
|
190
|
+
|
|
191
|
+
<div className="flex items-center gap-1.5 self-end pb-0.5">
|
|
192
|
+
{collapsible && (
|
|
193
|
+
<button
|
|
194
|
+
type="button"
|
|
195
|
+
className="pw-deck-action-btn"
|
|
196
|
+
data-testid="uploads-toggle"
|
|
197
|
+
aria-expanded={expanded}
|
|
198
|
+
onClick={() => {
|
|
199
|
+
setExpanded((open) => !open);
|
|
200
|
+
setLifted(null);
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
{expanded ? t.chipCollapse : t.chipExpandMore.replace("{n}", String(files.length - COLLAPSE_AT))}
|
|
204
|
+
</button>
|
|
205
|
+
)}
|
|
206
|
+
|
|
207
|
+
{files.length > 1 && onClear && (
|
|
208
|
+
<button
|
|
209
|
+
type="button"
|
|
210
|
+
className="pw-deck-action-btn pw-deck-action-clear"
|
|
211
|
+
data-testid="uploads-clear"
|
|
212
|
+
disabled={disabled}
|
|
213
|
+
onClick={() => {
|
|
214
|
+
onClear();
|
|
215
|
+
setLifted(null);
|
|
216
|
+
}}
|
|
217
|
+
>
|
|
218
|
+
{t.filesClearAll}
|
|
219
|
+
</button>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { useEffect, useRef, useState } from "react";
|
|
16
16
|
import { createPortal } from "react-dom";
|
|
17
|
+
import { ImageViewer } from "@/components/ImageViewer";
|
|
17
18
|
import { useI18n } from "@/i18n";
|
|
18
19
|
import type { ImageAttachment } from "@/lib/types";
|
|
19
20
|
|
|
@@ -61,27 +62,35 @@ export function deckLayout(count: number): Layout {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
65
|
+
* 扇形:沿竖直向上逐级延伸呈弧度展开(与多文件完全同一套竖直向上延展体验)。
|
|
66
|
+
* 卡片从底部逐级向上攀升,每张卡片竖直排布并沿轻微弧线微幅偏转,悬停上浮高亮。
|
|
66
67
|
*/
|
|
67
68
|
export function fanLayout(count: number, width: number): Layout {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
const
|
|
69
|
+
if (count <= 1) {
|
|
70
|
+
return { cards: [{ x: FAN_SIDE, y: 0, angle: 0 }], width: CARD + FAN_SIDE * 2, height: CARD, gap: 0 };
|
|
71
|
+
}
|
|
72
|
+
// 竖直向上延伸:每张卡片向上抬升 stepY
|
|
73
|
+
const stepY = Math.min(36, Math.max(16, 210 / (count - 1)));
|
|
74
|
+
const spanY = (count - 1) * stepY;
|
|
75
|
+
const totalH = CARD + spanY + 14;
|
|
75
76
|
const cards = Array.from({ length: count }, (_, i) => {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
const progress = i / (count - 1);
|
|
78
|
+
// y 轴:Card 0 在底部,Card count-1 在最上方(竖直向上延伸)
|
|
79
|
+
const y = round((totalH - CARD - 6) - i * stepY);
|
|
80
|
+
// x 轴:向上延伸的同时呈柔和弧线向右微弯再回收(优雅的 C 弧)
|
|
81
|
+
const arcX = 22 * Math.sin(progress * Math.PI);
|
|
82
|
+
const x = round(FAN_SIDE + arcX + progress * 8);
|
|
83
|
+
// 角度:从下方的 -4° 顺滑过渡到上方的 +6°
|
|
84
|
+
const angle = round(-4 + progress * 10);
|
|
85
|
+
return { x, y, angle };
|
|
82
86
|
});
|
|
83
|
-
const
|
|
84
|
-
return {
|
|
87
|
+
const maxX = Math.max(...cards.map((c) => c.x)) + CARD + FAN_SIDE;
|
|
88
|
+
return {
|
|
89
|
+
cards,
|
|
90
|
+
width: Math.min(width, Math.max(CARD + 32, Math.ceil(maxX))),
|
|
91
|
+
height: Math.ceil(totalH),
|
|
92
|
+
gap: round(stepY),
|
|
93
|
+
};
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
/** 每张图一个稳定的 key:前几位 base64 对同格式的图几乎都一样(PNG 都是 iVBORw0KGgo…),不能拿来当 key */
|
|
@@ -140,7 +149,7 @@ export function ComposerImages({ images, disabled, onRemove, onClear }: { images
|
|
|
140
149
|
|
|
141
150
|
return (
|
|
142
151
|
<>
|
|
143
|
-
<div ref={railRef} className="pointer-events-auto flex flex-wrap items-
|
|
152
|
+
<div ref={railRef} className="pointer-events-auto flex flex-wrap items-end gap-2 px-1 pb-1" data-testid="composer-images">
|
|
144
153
|
{collapsible ? (
|
|
145
154
|
<div
|
|
146
155
|
className="pw-image-fan"
|
|
@@ -169,7 +178,7 @@ export function ComposerImages({ images, disabled, onRemove, onClear }: { images
|
|
|
169
178
|
data-lifted={up || undefined}
|
|
170
179
|
style={{
|
|
171
180
|
zIndex: up ? images.length + 1 : images.length - i,
|
|
172
|
-
transform: `translate(${pose.x}px, ${pose.y - (up ? FAN_LIFT : 0)}px) rotate(${pose.angle}deg)`,
|
|
181
|
+
transform: `translate(${pose.x}px, ${pose.y - (up ? FAN_LIFT : 0)}px) rotate(${pose.angle}deg) ${up ? "scale(1.05)" : ""}`,
|
|
173
182
|
transitionDelay: up || lifted !== null ? "0ms" : `${delay}ms`,
|
|
174
183
|
}}
|
|
175
184
|
onMouseEnter={() => fanned && setLifted(i)}
|
|
@@ -206,35 +215,37 @@ export function ComposerImages({ images, disabled, onRemove, onClear }: { images
|
|
|
206
215
|
</div>
|
|
207
216
|
))
|
|
208
217
|
)}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
218
|
+
<div className="flex items-center gap-1.5 self-end pb-0.5">
|
|
219
|
+
{collapsible ? (
|
|
220
|
+
<button
|
|
221
|
+
type="button"
|
|
222
|
+
className="pw-deck-action-btn"
|
|
223
|
+
data-testid="images-toggle"
|
|
224
|
+
aria-expanded={fanned}
|
|
225
|
+
onClick={() => {
|
|
226
|
+
setExpanded((open) => !open);
|
|
227
|
+
setLifted(null);
|
|
228
|
+
}}
|
|
229
|
+
>
|
|
230
|
+
{fanned ? t.chipCollapse : t.chipExpandMore.replace("{n}", String(images.length - 1))}
|
|
231
|
+
</button>
|
|
232
|
+
) : null}
|
|
233
|
+
{images.length > 1 && onClear ? (
|
|
234
|
+
<button
|
|
235
|
+
type="button"
|
|
236
|
+
className="pw-deck-action-btn pw-deck-action-clear"
|
|
237
|
+
data-testid="images-clear"
|
|
238
|
+
disabled={disabled}
|
|
239
|
+
onClick={() => {
|
|
240
|
+
onClear();
|
|
241
|
+
setLifted(null);
|
|
242
|
+
setPreview(null);
|
|
243
|
+
}}
|
|
244
|
+
>
|
|
245
|
+
{t.imageClearAll}
|
|
246
|
+
</button>
|
|
247
|
+
) : null}
|
|
248
|
+
</div>
|
|
238
249
|
</div>
|
|
239
250
|
{shown !== null
|
|
240
251
|
? createPortal(
|
|
@@ -245,82 +256,3 @@ export function ComposerImages({ images, disabled, onRemove, onClear }: { images
|
|
|
245
256
|
</>
|
|
246
257
|
);
|
|
247
258
|
}
|
|
248
|
-
|
|
249
|
-
/** 放大预览:手机上没法用本机程序打开图片,应用内看最实在 */
|
|
250
|
-
function ImageViewer({
|
|
251
|
-
images,
|
|
252
|
-
index,
|
|
253
|
-
disabled,
|
|
254
|
-
onIndex,
|
|
255
|
-
onRemove,
|
|
256
|
-
onClose,
|
|
257
|
-
}: {
|
|
258
|
-
images: ImageAttachment[];
|
|
259
|
-
index: number;
|
|
260
|
-
disabled?: boolean;
|
|
261
|
-
onIndex: (index: number) => void;
|
|
262
|
-
onRemove: (index: number) => void;
|
|
263
|
-
onClose: () => void;
|
|
264
|
-
}) {
|
|
265
|
-
const { t } = useI18n();
|
|
266
|
-
const dialogRef = useRef<HTMLDivElement>(null);
|
|
267
|
-
const closeRef = useRef<HTMLButtonElement>(null);
|
|
268
|
-
const count = images.length;
|
|
269
|
-
const go = (delta: number) => onIndex((index + delta + count) % count);
|
|
270
|
-
|
|
271
|
-
// 打开时把焦点移进来,关掉时还给原来那个元素(还在页面上的话)
|
|
272
|
-
useEffect(() => {
|
|
273
|
-
const opener = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
274
|
-
closeRef.current?.focus();
|
|
275
|
-
return () => {
|
|
276
|
-
if (opener?.isConnected) opener.focus();
|
|
277
|
-
};
|
|
278
|
-
}, []);
|
|
279
|
-
|
|
280
|
-
// 捕获阶段处理:Esc / 方向键不再传给页面上其它监听 document 的面板;Tab 只在预览里转
|
|
281
|
-
useEffect(() => {
|
|
282
|
-
const onKey = (event: KeyboardEvent) => {
|
|
283
|
-
if (event.key === "Escape") onClose();
|
|
284
|
-
else if (event.key === "ArrowLeft" && count > 1) go(-1);
|
|
285
|
-
else if (event.key === "ArrowRight" && count > 1) go(1);
|
|
286
|
-
else if (event.key === "Tab") {
|
|
287
|
-
const focusable = [...(dialogRef.current?.querySelectorAll<HTMLElement>("button:not(:disabled)") ?? [])];
|
|
288
|
-
if (!focusable.length) return;
|
|
289
|
-
const at = focusable.indexOf(document.activeElement as HTMLElement);
|
|
290
|
-
const next = event.shiftKey ? (at <= 0 ? focusable.length - 1 : at - 1) : at === focusable.length - 1 ? 0 : at + 1;
|
|
291
|
-
focusable[next].focus();
|
|
292
|
-
} else return;
|
|
293
|
-
event.preventDefault();
|
|
294
|
-
event.stopPropagation();
|
|
295
|
-
};
|
|
296
|
-
document.addEventListener("keydown", onKey, true);
|
|
297
|
-
return () => document.removeEventListener("keydown", onKey, true);
|
|
298
|
-
// onClose / onIndex 来自父组件的 setState 包装,语义不变;只随张数和当前张重绑
|
|
299
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
300
|
-
}, [count, index]);
|
|
301
|
-
|
|
302
|
-
return (
|
|
303
|
-
<div ref={dialogRef} className="pw-lightbox" role="dialog" aria-modal="true" aria-label={t.imageViewer} onClick={onClose}>
|
|
304
|
-
<div className="pw-lightbox-bar" onClick={(event) => event.stopPropagation()}>
|
|
305
|
-
<span className="pw-lightbox-count">{count > 1 ? `${index + 1} / ${count}` : ""}</span>
|
|
306
|
-
<button type="button" className="pw-lightbox-btn" disabled={disabled} onClick={() => onRemove(index)}>
|
|
307
|
-
{t.delete}
|
|
308
|
-
</button>
|
|
309
|
-
<button ref={closeRef} type="button" className="pw-lightbox-btn" aria-label={t.close} onClick={onClose}>
|
|
310
|
-
✕
|
|
311
|
-
</button>
|
|
312
|
-
</div>
|
|
313
|
-
<img src={srcOf(images[index])} alt="" className="pw-lightbox-image" onClick={(event) => event.stopPropagation()} />
|
|
314
|
-
{count > 1 ? (
|
|
315
|
-
<>
|
|
316
|
-
<button type="button" className="pw-lightbox-nav" data-side="prev" aria-label={t.imagePrev} onClick={(event) => { event.stopPropagation(); go(-1); }}>
|
|
317
|
-
‹
|
|
318
|
-
</button>
|
|
319
|
-
<button type="button" className="pw-lightbox-nav" data-side="next" aria-label={t.imageNext} onClick={(event) => { event.stopPropagation(); go(1); }}>
|
|
320
|
-
›
|
|
321
|
-
</button>
|
|
322
|
-
</>
|
|
323
|
-
) : null}
|
|
324
|
-
</div>
|
|
325
|
-
);
|
|
326
|
-
}
|
package/src/hooks/usePiWeb.ts
CHANGED
|
@@ -44,6 +44,17 @@ export interface ExtensionDialog {
|
|
|
44
44
|
ts: number;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/** 右下角的短通知:扩展 notify,或 PiWeb 自己的提示(带 code,渲染时按语言取文案) */
|
|
48
|
+
export interface PiWebNotice {
|
|
49
|
+
id: string;
|
|
50
|
+
message: string;
|
|
51
|
+
type: "info" | "warning" | "error";
|
|
52
|
+
ts: number;
|
|
53
|
+
/** external_reload:终端写了这个会话,已从磁盘重载;external_write:正在跑时终端也在写 */
|
|
54
|
+
code?: "external_reload" | "external_write";
|
|
55
|
+
count?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
export interface PiWebState {
|
|
48
59
|
snapshot: WebSnapshot | null;
|
|
49
60
|
messages: WebMessage[];
|
|
@@ -57,7 +68,7 @@ export interface PiWebState {
|
|
|
57
68
|
/** 扩展对话框队列(按到达顺序,一次显示一个) */
|
|
58
69
|
extensionDialogs: ExtensionDialog[];
|
|
59
70
|
/** 扩展通知(notify),显示几秒后消失 */
|
|
60
|
-
extensionNotices:
|
|
71
|
+
extensionNotices: PiWebNotice[];
|
|
61
72
|
/** 扩展设的页脚状态文本(setStatus) */
|
|
62
73
|
extensionStatuses: Record<string, string>;
|
|
63
74
|
/** 扩展覆盖的「工作中」文案(setWorkingMessage) */
|
|
@@ -176,6 +187,17 @@ export const foldPiWebEvent = (state: PiWebState, evt: WebEvent): PiWebState =>
|
|
|
176
187
|
return s;
|
|
177
188
|
case "compaction":
|
|
178
189
|
s.compaction = { phase: evt.phase, errorMessage: evt.errorMessage, ts: evt.ts };
|
|
190
|
+
if (evt.marker) {
|
|
191
|
+
// 已显示的消息全部保留;分隔卡插到保留下来的第一条前面,和刷新后的快照位置一致
|
|
192
|
+
const marker = evt.marker;
|
|
193
|
+
const msgs = s.messages.filter((message) => !(marker.id && message.id === marker.id));
|
|
194
|
+
const at = msgs.findIndex((message) =>
|
|
195
|
+
(!!evt.beforeId && message.id === evt.beforeId) ||
|
|
196
|
+
(!!evt.beforeStreamId && message.streamId === evt.beforeStreamId));
|
|
197
|
+
if (at >= 0) msgs.splice(at, 0, marker);
|
|
198
|
+
else msgs.push(marker);
|
|
199
|
+
s.messages = msgs;
|
|
200
|
+
}
|
|
179
201
|
return s;
|
|
180
202
|
case "traj": {
|
|
181
203
|
if (s.snapshot) {
|
|
@@ -236,6 +258,12 @@ export const foldPiWebEvent = (state: PiWebState, evt: WebEvent): PiWebState =>
|
|
|
236
258
|
case "extension_ui_resolved":
|
|
237
259
|
s.extensionDialogs = s.extensionDialogs.filter((dialog) => dialog.id !== evt.id);
|
|
238
260
|
return s;
|
|
261
|
+
case "external_write":
|
|
262
|
+
s.extensionNotices = [
|
|
263
|
+
...s.extensionNotices.filter((notice) => notice.code !== "external_write").slice(-4),
|
|
264
|
+
{ id: `external-write-${evt.ts}`, message: "", type: "warning", ts: evt.ts, code: "external_write", count: evt.entries },
|
|
265
|
+
];
|
|
266
|
+
return s;
|
|
239
267
|
case "status":
|
|
240
268
|
if (s.snapshot) s.snapshot = { ...s.snapshot, isStreaming: evt.isStreaming };
|
|
241
269
|
if (!evt.isStreaming) {
|
|
@@ -366,6 +394,7 @@ export function usePiWeb() {
|
|
|
366
394
|
const [snapshotEpoch, setSnapshotEpoch] = useState(0);
|
|
367
395
|
const esRef = useRef<{ close(): void } | null>(null);
|
|
368
396
|
const promptRequestsRef = useRef(new Set<string>());
|
|
397
|
+
const shownReloadsRef = useRef(new Set<string>());
|
|
369
398
|
const presetRestoreAttemptRef = useRef<string | null>(null);
|
|
370
399
|
const presetRestorePromiseRef = useRef<Promise<void> | null>(null);
|
|
371
400
|
const activeSessionRef = useRef(currentId);
|
|
@@ -689,6 +718,14 @@ export function usePiWeb() {
|
|
|
689
718
|
: { ...emptyState(savedToolPreset()), connected: false });
|
|
690
719
|
let retryDelayMs = 2000;
|
|
691
720
|
const eventsUrl = `/api/agent/${currentId}/events`;
|
|
721
|
+
// 同一次重载既有 external_reload 事件、又会出现在随后若干快照里:按重载时间只提示一次
|
|
722
|
+
const reloadNotice = (reload?: { entries: number; at: number }): PiWebNotice | null => {
|
|
723
|
+
if (!reload) return null;
|
|
724
|
+
const key = `${currentId}:${reload.at}`;
|
|
725
|
+
if (shownReloadsRef.current.has(key)) return null;
|
|
726
|
+
shownReloadsRef.current.add(key);
|
|
727
|
+
return { id: `external-reload-${reload.at}`, message: "", type: "info", ts: Date.now(), code: "external_reload", count: reload.entries };
|
|
728
|
+
};
|
|
692
729
|
const connect = () => {
|
|
693
730
|
// 流式事件按帧合并:token 速率再高也只每 16ms 渲染一次(见 event-batcher)
|
|
694
731
|
const batcher = createEventBatcher<WebEvent>({
|
|
@@ -707,11 +744,19 @@ export function usePiWeb() {
|
|
|
707
744
|
// 单帧损坏(代理截断等)只丢弃该帧,不能让 EventSource 回调抛异常
|
|
708
745
|
return;
|
|
709
746
|
}
|
|
747
|
+
if (parsed.type === "external_reload") {
|
|
748
|
+
// 服务端发现终端写过这个会话、已从磁盘重载,旧流随即关闭:不等 EventSource 2 秒后自己重连,马上换新连接拿快照
|
|
749
|
+
const notice = reloadNotice(parsed);
|
|
750
|
+
if (notice) setState((s) => ({ ...s, extensionNotices: [...s.extensionNotices.slice(-4), notice] }));
|
|
751
|
+
setResyncNonce((n) => n + 1);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
710
754
|
if (parsed.type === "snapshot" && parsed.snapshot) {
|
|
711
755
|
// 快照是权威状态:之前缓冲的增量已经过时,直接丢弃
|
|
712
756
|
batcher.cancel();
|
|
713
757
|
retryDelayMs = 2000;
|
|
714
758
|
const snap = parsed.snapshot as WebSnapshot;
|
|
759
|
+
const notice = reloadNotice(snap.externalReload);
|
|
715
760
|
setSnapshotEpoch((epoch) => epoch + 1);
|
|
716
761
|
setState((prev) => ({
|
|
717
762
|
snapshot: snap,
|
|
@@ -724,7 +769,7 @@ export function usePiWeb() {
|
|
|
724
769
|
retryNotice: null,
|
|
725
770
|
// 服务器当前 pending 集合是权威来源,刷新可恢复、已答请求不能复活。
|
|
726
771
|
extensionDialogs: extensionDialogsFromSnapshot(snap, prev.extensionDialogs),
|
|
727
|
-
extensionNotices: prev.extensionNotices,
|
|
772
|
+
extensionNotices: notice ? [...prev.extensionNotices.slice(-4), notice] : prev.extensionNotices,
|
|
728
773
|
extensionStatuses: snap.extensionStatuses ?? {},
|
|
729
774
|
workingMessage: null,
|
|
730
775
|
// 重连的快照不带生长账本;连接期间已收到的步保留,useGrowth 会按 seq 与拉取结果去重
|