@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,142 @@
|
|
|
1
|
+
.wrap {
|
|
2
|
+
position: relative;
|
|
3
|
+
display: inline-block;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.trigger {
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
justify-content: space-between;
|
|
10
|
+
gap: 6px;
|
|
11
|
+
border: 0.5px solid var(--dsw-border-l2);
|
|
12
|
+
background: var(--dsw-glass-input);
|
|
13
|
+
color: var(--dsw-label-primary);
|
|
14
|
+
cursor: pointer;
|
|
15
|
+
text-align: left;
|
|
16
|
+
user-select: none;
|
|
17
|
+
transition: border-color 0.18s var(--isx-ease), box-shadow 0.18s var(--isx-ease), background 0.18s var(--isx-ease);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.trigger:hover:not(:disabled) {
|
|
21
|
+
border-color: var(--dsw-border-l3);
|
|
22
|
+
background: var(--dsw-bg-elevated);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.trigger:focus-visible,
|
|
26
|
+
.trigger[data-open="true"] {
|
|
27
|
+
border-color: var(--dsw-accent);
|
|
28
|
+
box-shadow: 0 0 0 2px var(--dsw-accent-soft);
|
|
29
|
+
outline: none;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.trigger:disabled {
|
|
33
|
+
opacity: 0.45;
|
|
34
|
+
cursor: not-allowed;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.sizeSm {
|
|
38
|
+
min-height: 32px;
|
|
39
|
+
padding: 0 9px;
|
|
40
|
+
font-size: 12.5px;
|
|
41
|
+
border-radius: 10px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.sizeMd {
|
|
45
|
+
width: 100%;
|
|
46
|
+
min-height: 42px;
|
|
47
|
+
padding: 0 13px;
|
|
48
|
+
font-size: 13.5px;
|
|
49
|
+
border-radius: 13px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.label {
|
|
53
|
+
min-width: 0;
|
|
54
|
+
flex: 1;
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
text-overflow: ellipsis;
|
|
57
|
+
white-space: nowrap;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.chevron {
|
|
61
|
+
flex: none;
|
|
62
|
+
color: var(--dsw-label-secondary);
|
|
63
|
+
transition: transform 0.2s var(--isx-ease);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.chevronOpen {
|
|
67
|
+
transform: rotate(180deg);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.menu {
|
|
71
|
+
position: absolute;
|
|
72
|
+
top: calc(100% + 5px);
|
|
73
|
+
left: 0;
|
|
74
|
+
min-width: 100%;
|
|
75
|
+
width: max-content;
|
|
76
|
+
max-width: 300px;
|
|
77
|
+
z-index: 100;
|
|
78
|
+
max-height: 220px;
|
|
79
|
+
overflow-y: auto;
|
|
80
|
+
padding: 4px;
|
|
81
|
+
border: 0.5px solid var(--dsw-border-l2);
|
|
82
|
+
border-radius: 12px;
|
|
83
|
+
background: var(--dsw-glass-popover);
|
|
84
|
+
backdrop-filter: blur(20px);
|
|
85
|
+
-webkit-backdrop-filter: blur(20px);
|
|
86
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
|
|
87
|
+
animation: popover-in 0.15s cubic-bezier(0.16, 1, 0.3, 1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.menuFlip {
|
|
91
|
+
top: auto;
|
|
92
|
+
bottom: calc(100% + 5px);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@keyframes popover-in {
|
|
96
|
+
from {
|
|
97
|
+
opacity: 0;
|
|
98
|
+
transform: translateY(-4px) scale(0.98);
|
|
99
|
+
}
|
|
100
|
+
to {
|
|
101
|
+
opacity: 1;
|
|
102
|
+
transform: translateY(0) scale(1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.option {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
justify-content: space-between;
|
|
110
|
+
gap: 8px;
|
|
111
|
+
width: 100%;
|
|
112
|
+
padding: 6px 9px;
|
|
113
|
+
border-radius: 8px;
|
|
114
|
+
font-size: 12.5px;
|
|
115
|
+
color: var(--dsw-label-primary);
|
|
116
|
+
background: transparent;
|
|
117
|
+
border: none;
|
|
118
|
+
cursor: pointer;
|
|
119
|
+
text-align: left;
|
|
120
|
+
user-select: none;
|
|
121
|
+
transition: background 0.12s ease, color 0.12s ease;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.option:hover:not(:disabled) {
|
|
125
|
+
background: var(--dsw-hover);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.optionSelected {
|
|
129
|
+
color: var(--dsw-accent);
|
|
130
|
+
font-weight: 550;
|
|
131
|
+
background: var(--dsw-accent-soft);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.option:disabled {
|
|
135
|
+
opacity: 0.4;
|
|
136
|
+
cursor: not-allowed;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.check {
|
|
140
|
+
flex: none;
|
|
141
|
+
color: var(--dsw-accent);
|
|
142
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState, type CSSProperties, type KeyboardEvent } from "react";
|
|
4
|
+
import { IconCheckOutline14, IconChevronDown14 } from "@/components/icons";
|
|
5
|
+
import styles from "./CustomSelect.module.css";
|
|
6
|
+
|
|
7
|
+
export interface SelectOption {
|
|
8
|
+
value: string;
|
|
9
|
+
label: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CustomSelectProps {
|
|
14
|
+
value: string;
|
|
15
|
+
onChange: (value: string) => void;
|
|
16
|
+
options: SelectOption[];
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
size?: "sm" | "md";
|
|
20
|
+
className?: string;
|
|
21
|
+
style?: CSSProperties;
|
|
22
|
+
"aria-label"?: string;
|
|
23
|
+
"data-testid"?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function CustomSelect({
|
|
27
|
+
value,
|
|
28
|
+
onChange,
|
|
29
|
+
options,
|
|
30
|
+
placeholder,
|
|
31
|
+
disabled = false,
|
|
32
|
+
size = "md",
|
|
33
|
+
className = "",
|
|
34
|
+
style,
|
|
35
|
+
"aria-label": ariaLabel,
|
|
36
|
+
"data-testid": testId,
|
|
37
|
+
}: CustomSelectProps) {
|
|
38
|
+
const [open, setOpen] = useState(false);
|
|
39
|
+
const [flip, setFlip] = useState(false);
|
|
40
|
+
const wrapRef = useRef<HTMLDivElement>(null);
|
|
41
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
42
|
+
|
|
43
|
+
const selectedOption = options.find((opt) => opt.value === value);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!open) return;
|
|
47
|
+
// 检查下方空间是否足够,不足则向上展开
|
|
48
|
+
if (triggerRef.current) {
|
|
49
|
+
const rect = triggerRef.current.getBoundingClientRect();
|
|
50
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
51
|
+
setFlip(spaceBelow < 220 && rect.top > 220);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const handlePointerDown = (e: MouseEvent) => {
|
|
55
|
+
if (!wrapRef.current?.contains(e.target as Node)) {
|
|
56
|
+
setOpen(false);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handleKeyDownDoc = (e: globalThis.KeyboardEvent) => {
|
|
61
|
+
if (e.key === "Escape") {
|
|
62
|
+
setOpen(false);
|
|
63
|
+
triggerRef.current?.focus();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
document.addEventListener("mousedown", handlePointerDown);
|
|
68
|
+
document.addEventListener("keydown", handleKeyDownDoc);
|
|
69
|
+
return () => {
|
|
70
|
+
document.removeEventListener("mousedown", handlePointerDown);
|
|
71
|
+
document.removeEventListener("keydown", handleKeyDownDoc);
|
|
72
|
+
};
|
|
73
|
+
}, [open]);
|
|
74
|
+
|
|
75
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {
|
|
76
|
+
if (disabled) return;
|
|
77
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Enter" || e.key === " ") {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
setOpen((prev) => !prev);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div ref={wrapRef} className={`${styles.wrap} ${className}`} style={style}>
|
|
85
|
+
{/* 原生隐藏 select:保证测试、表单无障碍与自动化脚本完全兼容 */}
|
|
86
|
+
<select
|
|
87
|
+
tabIndex={-1}
|
|
88
|
+
value={value}
|
|
89
|
+
disabled={disabled}
|
|
90
|
+
aria-label={ariaLabel}
|
|
91
|
+
onChange={(e) => onChange(e.target.value)}
|
|
92
|
+
style={{
|
|
93
|
+
position: "absolute",
|
|
94
|
+
width: 1,
|
|
95
|
+
height: 1,
|
|
96
|
+
padding: 0,
|
|
97
|
+
margin: -1,
|
|
98
|
+
overflow: "hidden",
|
|
99
|
+
clip: "rect(0, 0, 0, 0)",
|
|
100
|
+
whiteSpace: "nowrap",
|
|
101
|
+
border: 0,
|
|
102
|
+
opacity: 0,
|
|
103
|
+
pointerEvents: "none",
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
{options.map((opt) => (
|
|
107
|
+
<option key={opt.value} value={opt.value} disabled={opt.disabled}>
|
|
108
|
+
{opt.label}
|
|
109
|
+
</option>
|
|
110
|
+
))}
|
|
111
|
+
</select>
|
|
112
|
+
|
|
113
|
+
<button
|
|
114
|
+
ref={triggerRef}
|
|
115
|
+
type="button"
|
|
116
|
+
disabled={disabled}
|
|
117
|
+
data-open={open ? "true" : undefined}
|
|
118
|
+
data-testid={testId}
|
|
119
|
+
aria-label={ariaLabel}
|
|
120
|
+
aria-haspopup="listbox"
|
|
121
|
+
aria-expanded={open}
|
|
122
|
+
className={`${styles.trigger} ${size === "sm" ? styles.sizeSm : styles.sizeMd}`}
|
|
123
|
+
onClick={() => setOpen((prev) => !prev)}
|
|
124
|
+
onKeyDown={handleKeyDown}
|
|
125
|
+
>
|
|
126
|
+
<span className={styles.label}>{selectedOption?.label ?? placeholder ?? value}</span>
|
|
127
|
+
<IconChevronDown14 size={size === "sm" ? 13 : 15} className={`${styles.chevron} ${open ? styles.chevronOpen : ""}`} />
|
|
128
|
+
</button>
|
|
129
|
+
|
|
130
|
+
{open && (
|
|
131
|
+
<div className={`${styles.menu} ${flip ? styles.menuFlip : ""}`} role="listbox" aria-label={ariaLabel}>
|
|
132
|
+
{options.map((opt) => {
|
|
133
|
+
const isSelected = opt.value === value;
|
|
134
|
+
return (
|
|
135
|
+
<button
|
|
136
|
+
key={opt.value}
|
|
137
|
+
type="button"
|
|
138
|
+
role="option"
|
|
139
|
+
aria-selected={isSelected}
|
|
140
|
+
disabled={opt.disabled}
|
|
141
|
+
className={`${styles.option} ${isSelected ? styles.optionSelected : ""}`}
|
|
142
|
+
onClick={() => {
|
|
143
|
+
onChange(opt.value);
|
|
144
|
+
setOpen(false);
|
|
145
|
+
triggerRef.current?.focus();
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
<span className="truncate">{opt.label}</span>
|
|
149
|
+
{isSelected ? <IconCheckOutline14 size={14} className={styles.check} /> : null}
|
|
150
|
+
</button>
|
|
151
|
+
);
|
|
152
|
+
})}
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
@@ -18,7 +18,7 @@ import type { GrowthApi } from "@/hooks/useGrowth";
|
|
|
18
18
|
import { useI18n } from "@/i18n";
|
|
19
19
|
import type { TreeNode } from "@/lib/growth-tree";
|
|
20
20
|
|
|
21
|
-
/** 抽屉入口:项目 → 提示词 → Git 详情 →
|
|
21
|
+
/** 抽屉入口:项目 → 提示词 → Git 详情 → 会话导入。 */
|
|
22
22
|
export type DrawerSection = "project" | "prompts" | "git" | "import";
|
|
23
23
|
|
|
24
24
|
export function DrawerPanel({
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
8
8
|
import { IconShieldOutline16 } from "@/components/icons";
|
|
9
9
|
import { useI18n } from "@/i18n";
|
|
10
|
-
import type { ExtensionDialog } from "@/hooks/usePiWeb";
|
|
10
|
+
import type { ExtensionDialog, PiWebNotice } from "@/hooks/usePiWeb";
|
|
11
11
|
|
|
12
12
|
/** message 若全是 "key : value" 行(权限对话框的常见形态),解析成键值表渲染 */
|
|
13
13
|
function parseKeyValueRows(message: string): { key: string; value: string }[] | null {
|
|
@@ -154,11 +154,17 @@ export function ExtensionNotices({
|
|
|
154
154
|
onDismiss,
|
|
155
155
|
children,
|
|
156
156
|
}: {
|
|
157
|
-
notices:
|
|
157
|
+
notices: PiWebNotice[];
|
|
158
158
|
onDismiss: (id: string) => void;
|
|
159
159
|
/** 同一摞里的其它常驻提示(如更新提示条),排在扩展通知上面,免得两处浮层叠在一起 */
|
|
160
160
|
children?: React.ReactNode;
|
|
161
161
|
}) {
|
|
162
|
+
const { t } = useI18n();
|
|
163
|
+
const text = (n: PiWebNotice) => {
|
|
164
|
+
if (n.code === "external_reload") return t.externalSessionReloaded.replace("{n}", String(n.count ?? 0));
|
|
165
|
+
if (n.code === "external_write") return t.externalSessionWriting.replace("{n}", String(n.count ?? 0));
|
|
166
|
+
return n.message;
|
|
167
|
+
};
|
|
162
168
|
// 通知 6 秒后自动消失
|
|
163
169
|
useEffect(() => {
|
|
164
170
|
if (!notices.length) return;
|
|
@@ -179,7 +185,7 @@ export function ExtensionNotices({
|
|
|
179
185
|
style={{ fontSize: 12.5, background: "var(--dsw-glass-popover)", border: "0.5px solid var(--dsw-border-l2)", boxShadow: "var(--dsw-elevation-soft)", borderLeft: `3px solid ${color(n.type)}` }}
|
|
180
186
|
onClick={() => onDismiss(n.id)}
|
|
181
187
|
>
|
|
182
|
-
{n
|
|
188
|
+
{text(n)}
|
|
183
189
|
</button>
|
|
184
190
|
))}
|
|
185
191
|
</div>
|
|
@@ -17,6 +17,7 @@ import { IconAtOutline16, IconChevronDown14, IconChevronUp14, IconCloseOutline14
|
|
|
17
17
|
import type { GrowthApi } from "@/hooks/useGrowth";
|
|
18
18
|
import { useI18n } from "@/i18n";
|
|
19
19
|
import { fileIconSrc } from "@/lib/file-icons";
|
|
20
|
+
import { fmtBytes } from "@/lib/format";
|
|
20
21
|
import { fuzzyScore, type TreeNode } from "@/lib/growth-tree";
|
|
21
22
|
import type { ViewerState } from "@/hooks/useFileViewer";
|
|
22
23
|
import { highlightCode, languageForPath } from "@/lib/highlight";
|
|
@@ -113,8 +114,7 @@ export function FileViewer({
|
|
|
113
114
|
const [treeFilter, setTreeFilter] = useState("");
|
|
114
115
|
const [reveal, setReveal] = useState<{ path: string; key: number } | null>(null);
|
|
115
116
|
const [loaded, setLoaded] = useState<Loaded | null>(null);
|
|
116
|
-
const [dirPage, setDirPage] = useState<(DirPage & { key: string }) | null>(null);
|
|
117
|
-
const [dirMore, setDirMore] = useState<DirEntry[]>([]);
|
|
117
|
+
const [dirPage, setDirPage] = useState<(DirPage & { key: string; error?: string }) | null>(null);
|
|
118
118
|
const diffRef = useRef<FullDiffHandle>(null);
|
|
119
119
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
120
120
|
const quickInputRef = useRef<HTMLInputElement>(null);
|
|
@@ -138,41 +138,44 @@ export function FileViewer({
|
|
|
138
138
|
const [dirFallback, setDirFallback] = useState<string | null>(null);
|
|
139
139
|
const dirPath = activeTab?.kind === "dir" ? activeTab.path : dirFallback;
|
|
140
140
|
const isDir = Boolean(dirPath);
|
|
141
|
+
// @src/ 这类带尾斜杠的引用:列目录时去掉
|
|
142
|
+
const dirRel = dirPath ? dirPath.replace(/\/+$/, "") : null;
|
|
143
|
+
const dirKey = dirRel !== null && cwd ? `dir|${cwd}|${dirRel}` : null;
|
|
144
|
+
// 只认当前目录的那一页:从一个文件夹页签切到另一个时,上一个目录的条目不能在新请求回来前冒充新目录
|
|
145
|
+
// (点上去会拼出不存在的路径),「加载更多」也不能接着上一个目录的偏移拉
|
|
146
|
+
const page = dirPage && dirPage.key === dirKey ? dirPage : null;
|
|
141
147
|
// 文件夹:列目录(分页,后端已按 .gitignore 过滤),不读文件内容
|
|
142
148
|
useEffect(() => {
|
|
143
|
-
if (
|
|
149
|
+
if (dirRel === null || !cwd || !dirKey) {
|
|
144
150
|
setDirPage(null);
|
|
145
|
-
setDirMore([]);
|
|
146
151
|
return;
|
|
147
152
|
}
|
|
148
|
-
// @src/ 这类带尾斜杠的引用:列目录时去掉
|
|
149
|
-
const path_ = dirPath.replace(/\/+$/, "");
|
|
150
|
-
const key = `dir|${path_}`;
|
|
151
153
|
let alive = true;
|
|
152
|
-
setDirMore([]);
|
|
153
154
|
void (async () => {
|
|
154
155
|
try {
|
|
155
|
-
const data = await requestJson<DirPage>(`/api/files?cwd=${encodeURIComponent(cwd)}&path=${encodeURIComponent(
|
|
156
|
-
if (alive) setDirPage({ key, ...data });
|
|
156
|
+
const data = await requestJson<DirPage>(`/api/files?cwd=${encodeURIComponent(cwd)}&path=${encodeURIComponent(dirRel)}&offset=0`);
|
|
157
|
+
if (alive) setDirPage({ key: dirKey, ...data });
|
|
157
158
|
} catch (error) {
|
|
158
|
-
if (alive) setDirPage({ key, dir:
|
|
159
|
+
if (alive) setDirPage({ key: dirKey, dir: dirRel, entries: [], nextOffset: null, truncated: false, error: error instanceof Error ? error.message : t.viewerLoadFailed });
|
|
159
160
|
}
|
|
160
161
|
})();
|
|
161
162
|
return () => {
|
|
162
163
|
alive = false;
|
|
163
164
|
};
|
|
164
|
-
}, [
|
|
165
|
+
}, [dirRel, dirKey, cwd, t.viewerLoadFailed]);
|
|
165
166
|
|
|
166
167
|
/** 目录还有下一页时接着拉(大目录:node_modules 这种已被忽略,剩下的也不会太多) */
|
|
167
168
|
const loadMore = useCallback(async () => {
|
|
168
|
-
if (!
|
|
169
|
+
if (!page?.nextOffset || dirRel === null || !cwd) return;
|
|
170
|
+
const base = page;
|
|
169
171
|
try {
|
|
170
|
-
const data = await requestJson<DirPage>(`/api/files?cwd=${encodeURIComponent(cwd)}&path=${encodeURIComponent(
|
|
171
|
-
|
|
172
|
+
const data = await requestJson<DirPage>(`/api/files?cwd=${encodeURIComponent(cwd)}&path=${encodeURIComponent(dirRel)}&offset=${base.nextOffset}`);
|
|
173
|
+
// 请求途中切走了(或别的请求先换了这一页)就丢掉结果
|
|
174
|
+
setDirPage((current) => (current === base ? { ...base, ...data, key: base.key, entries: [...base.entries, ...data.entries] } : current));
|
|
172
175
|
} catch {
|
|
173
176
|
// 下一页失败就留在当前这页,用户可以再点
|
|
174
177
|
}
|
|
175
|
-
}, [
|
|
178
|
+
}, [page, dirRel, cwd]);
|
|
176
179
|
|
|
177
180
|
// 取数据
|
|
178
181
|
useEffect(() => {
|
|
@@ -497,10 +500,10 @@ export function FileViewer({
|
|
|
497
500
|
{loaded?.truncated && <div className="pw-viewer-notice" data-kind="warn">{mode === "diff" ? t.viewerPatchTruncated : t.filePreviewTruncated}</div>}
|
|
498
501
|
{isDir ? (
|
|
499
502
|
<div className="min-h-0 flex-1 overflow-auto" data-testid="viewer-dir">
|
|
500
|
-
<div className="px-3 py-2" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
|
|
501
|
-
{t.viewerDirHint.replace("{n}", String(
|
|
503
|
+
<div className="px-3 py-2" style={{ fontSize: 11.5, color: page?.error ? "var(--dsw-danger)" : "var(--dsw-label-caption)" }}>
|
|
504
|
+
{!page ? t.panelLoading : page.error ? page.error : t.viewerDirHint.replace("{n}", String(page.entries.length))}
|
|
502
505
|
</div>
|
|
503
|
-
{
|
|
506
|
+
{page?.entries.map((entry) => (
|
|
504
507
|
<button
|
|
505
508
|
key={entry.name}
|
|
506
509
|
type="button"
|
|
@@ -512,11 +515,11 @@ export function FileViewer({
|
|
|
512
515
|
<img className="pw-tree-icon" src={entry.kind === "dir" ? "/file-icons/folder-close.svg" : fileIconSrc(entry.name)} alt="" />
|
|
513
516
|
<span className="min-w-0 flex-1 truncate" style={{ fontSize: 12.5, fontFamily: entry.kind === "dir" ? "inherit" : "var(--font-mono)" }}>{entry.name}</span>
|
|
514
517
|
{entry.kind === "file" && entry.size !== undefined && (
|
|
515
|
-
<span style={{ fontSize: 11, color: "var(--dsw-label-caption)", fontVariantNumeric: "tabular-nums" }}>{entry.size}
|
|
518
|
+
<span style={{ fontSize: 11, color: "var(--dsw-label-caption)", fontVariantNumeric: "tabular-nums" }}>{fmtBytes(entry.size)}</span>
|
|
516
519
|
)}
|
|
517
520
|
</button>
|
|
518
521
|
))}
|
|
519
|
-
{
|
|
522
|
+
{page?.nextOffset ? (
|
|
520
523
|
<button type="button" className="pw-chip mx-3 my-2" onClick={() => void loadMore()}>{t.viewerDirMore}</button>
|
|
521
524
|
) : null}
|
|
522
525
|
</div>
|
|
@@ -329,7 +329,7 @@ export function GitPanel({
|
|
|
329
329
|
<div className="flex flex-wrap items-center gap-2" style={{ fontSize: 12.5 }}>
|
|
330
330
|
<span
|
|
331
331
|
className="flex items-center gap-1.5 rounded-full px-2.5 py-1"
|
|
332
|
-
style={{ background: "var(--dsw-accent-soft)", color: "var(--dsw-accent)", fontWeight:
|
|
332
|
+
style={{ background: "var(--dsw-accent-soft)", color: "var(--dsw-accent)", fontWeight: 600, border: "1px solid var(--dsw-border-l2)" }}
|
|
333
333
|
>
|
|
334
334
|
<IconGitOutline16 size={12} />
|
|
335
335
|
{info.detached ? t.gitDetached : info.branch ?? "—"}
|
|
@@ -364,20 +364,35 @@ export function GitPanel({
|
|
|
364
364
|
<span style={{ fontWeight: 600 }}>{area.label}</span>
|
|
365
365
|
<span>{area.entries.length}</span>
|
|
366
366
|
</div>
|
|
367
|
-
<div className="flex flex-col">
|
|
367
|
+
<div className="flex flex-col gap-0.5">
|
|
368
368
|
{area.entries.map(({ file: f, mode }) => (
|
|
369
369
|
<button
|
|
370
370
|
key={`${area.key}:${f.path}`}
|
|
371
371
|
type="button"
|
|
372
|
-
className="flex w-full items-center gap-2 rounded-
|
|
372
|
+
className="flex w-full items-center gap-2 rounded-xl px-2.5 py-1.5 text-left transition-all"
|
|
373
373
|
title={f.path}
|
|
374
|
-
|
|
375
|
-
|
|
374
|
+
style={{ transition: "all 0.18s var(--isx-ease)" }}
|
|
375
|
+
onMouseEnter={(e) => {
|
|
376
|
+
e.currentTarget.style.background = "var(--dsw-hover)";
|
|
377
|
+
e.currentTarget.style.transform = "translateX(2px)";
|
|
378
|
+
}}
|
|
379
|
+
onMouseLeave={(e) => {
|
|
380
|
+
e.currentTarget.style.background = "transparent";
|
|
381
|
+
e.currentTarget.style.transform = "none";
|
|
382
|
+
}}
|
|
376
383
|
onClick={() => openFile(f, mode, mode === "staged" ? t.gitStagedDiff : mode === "untracked" ? t.gitUntracked : t.gitWorktreeDiff)}
|
|
377
384
|
>
|
|
378
385
|
<span
|
|
379
|
-
className="flex-none rounded px-1 text-center"
|
|
380
|
-
style={{
|
|
386
|
+
className="flex-none rounded-full px-1.5 text-center"
|
|
387
|
+
style={{
|
|
388
|
+
minWidth: 18,
|
|
389
|
+
fontSize: 10,
|
|
390
|
+
fontFamily: "var(--font-mono)",
|
|
391
|
+
fontWeight: 700,
|
|
392
|
+
color: KIND_COLOR[f.kind],
|
|
393
|
+
background: "var(--dsw-hover)",
|
|
394
|
+
border: "1px solid currentColor",
|
|
395
|
+
}}
|
|
381
396
|
>
|
|
382
397
|
{f.kind === "untracked" ? "?" : f.kind === "conflict" ? "!" : mode === "staged" ? f.indexStatus : f.workStatus !== " " ? f.workStatus : f.indexStatus}
|
|
383
398
|
</span>
|
|
@@ -397,21 +412,28 @@ export function GitPanel({
|
|
|
397
412
|
{info.commits.length === 0 ? (
|
|
398
413
|
<div style={{ fontSize: 12.5, color: "var(--dsw-label-caption)", padding: "4px 2px" }}>{t.gitNoCommits}</div>
|
|
399
414
|
) : (
|
|
400
|
-
<div className="flex flex-col">
|
|
415
|
+
<div className="flex flex-col gap-0.5">
|
|
401
416
|
{info.commits.map((c) => (
|
|
402
417
|
<button
|
|
403
418
|
key={c.hash}
|
|
404
419
|
type="button"
|
|
405
|
-
className="flex w-full flex-col items-stretch rounded-
|
|
406
|
-
|
|
407
|
-
|
|
420
|
+
className="flex w-full flex-col items-stretch rounded-xl px-2.5 py-1.5 text-left transition-all"
|
|
421
|
+
style={{ transition: "all 0.18s var(--isx-ease)" }}
|
|
422
|
+
onMouseEnter={(e) => {
|
|
423
|
+
e.currentTarget.style.background = "var(--dsw-hover)";
|
|
424
|
+
e.currentTarget.style.transform = "translateX(2px)";
|
|
425
|
+
}}
|
|
426
|
+
onMouseLeave={(e) => {
|
|
427
|
+
e.currentTarget.style.background = "transparent";
|
|
428
|
+
e.currentTarget.style.transform = "none";
|
|
429
|
+
}}
|
|
408
430
|
onClick={() => changeView({ kind: "commit", hash: c.hash })}
|
|
409
431
|
>
|
|
410
432
|
<span className="flex items-baseline gap-2">
|
|
411
|
-
<span className="flex-none" style={{ fontSize:
|
|
433
|
+
<span className="flex-none rounded-md px-1.5 py-0.5" style={{ fontSize: 10.5, fontFamily: "var(--font-mono)", fontWeight: 600, color: "var(--dsw-accent)", background: "var(--dsw-accent-soft)" }}>{c.hash}</span>
|
|
412
434
|
<span className="min-w-0 flex-1 truncate" style={{ fontSize: 12, color: "var(--dsw-label-secondary)" }} title={c.subject}>{c.subject}</span>
|
|
413
435
|
</span>
|
|
414
|
-
<span className="truncate" style={{ fontSize: 11, color: "var(--dsw-label-caption)", paddingLeft: 0 }}>
|
|
436
|
+
<span className="truncate" style={{ fontSize: 11, color: "var(--dsw-label-caption)", paddingLeft: 0, marginTop: 2 }}>
|
|
415
437
|
{c.author} · {c.relative}
|
|
416
438
|
</span>
|
|
417
439
|
</button>
|
|
@@ -160,7 +160,9 @@ export const GrowthTree = memo(function GrowthTree({
|
|
|
160
160
|
return (
|
|
161
161
|
<div
|
|
162
162
|
ref={scrollRef}
|
|
163
|
-
|
|
163
|
+
// overflow-x-hidden 是必须的:.pw-tree-row:hover 会 translateX(2px),
|
|
164
|
+
// 单写 overflow-auto 时这 2px 会撑出横向滚动区,底部冒出一条滑块。
|
|
165
|
+
className="pw-tree min-h-0 flex-1 overflow-x-hidden overflow-y-auto outline-none"
|
|
164
166
|
tabIndex={0}
|
|
165
167
|
role="tree"
|
|
166
168
|
onKeyDown={onKeyDown}
|
|
@@ -274,7 +276,9 @@ const TreeRow = memo(function TreeRow({
|
|
|
274
276
|
{node.status === "R" && node.from && <span className="pw-tree-from">← {node.from.split("/").pop()}</span>}
|
|
275
277
|
</span>
|
|
276
278
|
{node.pending && <span className="pw-tree-pulse" aria-label={pendingLabel} />}
|
|
277
|
-
{
|
|
279
|
+
{/* 只有「磁盘上才有、快照里没有」的节点不给按钮。不能按 lazy 判断:快照里的目录只要磁盘上也有,
|
|
280
|
+
graftLazy 就会给它标 lazy(为了能展开读磁盘),根目录又总会被列出,顶层目录的按钮就全没了 */}
|
|
281
|
+
{!diskOnly && (onReference || (!isDir && onOpenEditor) || isDir) && (
|
|
278
282
|
<span className="pw-tree-actions">
|
|
279
283
|
{isDir && (
|
|
280
284
|
<button
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import { useI18n } from "@/i18n";
|
|
5
|
+
import type { ImageAttachment } from "@/lib/types";
|
|
6
|
+
|
|
7
|
+
export type PreviewImageSource = ImageAttachment | string;
|
|
8
|
+
export const imageSource = (image: PreviewImageSource) => typeof image === "string" ? image : `data:${image.mimeType};base64,${image.data}`;
|
|
9
|
+
|
|
10
|
+
/** 只读图集;删除只提供给尚未发送的输入附件(发出去的消息图片不可删) */
|
|
11
|
+
export function ImageViewer({ images, index, disabled, onIndex, onRemove, onClose }: {
|
|
12
|
+
images: PreviewImageSource[];
|
|
13
|
+
index: number;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
onIndex: (index: number) => void;
|
|
16
|
+
onRemove?: (index: number) => void;
|
|
17
|
+
onClose: () => void;
|
|
18
|
+
}) {
|
|
19
|
+
const { t } = useI18n();
|
|
20
|
+
const dialogRef = useRef<HTMLDivElement>(null);
|
|
21
|
+
const closeRef = useRef<HTMLButtonElement>(null);
|
|
22
|
+
const count = images.length;
|
|
23
|
+
const go = (delta: number) => onIndex((index + delta + count) % count);
|
|
24
|
+
|
|
25
|
+
// 打开时把焦点移进来、锁住页面滚动,关掉时把焦点还给原来那个元素(还在页面上的话)
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const opener = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
28
|
+
const overflow = document.body.style.overflow;
|
|
29
|
+
document.body.style.overflow = "hidden";
|
|
30
|
+
closeRef.current?.focus();
|
|
31
|
+
return () => {
|
|
32
|
+
document.body.style.overflow = overflow;
|
|
33
|
+
if (opener?.isConnected) opener.focus();
|
|
34
|
+
};
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
// 捕获阶段处理:Esc / 方向键不再传给页面上其它监听 document 的面板;Tab 只在预览里转
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const onKey = (event: KeyboardEvent) => {
|
|
40
|
+
if (event.key === "Escape") onClose();
|
|
41
|
+
else if (event.key === "ArrowLeft" && count > 1) go(-1);
|
|
42
|
+
else if (event.key === "ArrowRight" && count > 1) go(1);
|
|
43
|
+
else if (event.key === "Tab") {
|
|
44
|
+
const focusable = [...(dialogRef.current?.querySelectorAll<HTMLElement>("button:not(:disabled)") ?? [])];
|
|
45
|
+
if (!focusable.length) return;
|
|
46
|
+
const at = focusable.indexOf(document.activeElement as HTMLElement);
|
|
47
|
+
const next = event.shiftKey ? (at <= 0 ? focusable.length - 1 : at - 1) : at === focusable.length - 1 ? 0 : at + 1;
|
|
48
|
+
focusable[next].focus();
|
|
49
|
+
} else return;
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
event.stopPropagation();
|
|
52
|
+
};
|
|
53
|
+
document.addEventListener("keydown", onKey, true);
|
|
54
|
+
return () => document.removeEventListener("keydown", onKey, true);
|
|
55
|
+
}, [count, index, onClose, onIndex]);
|
|
56
|
+
|
|
57
|
+
if (!images[index]) return null;
|
|
58
|
+
return (
|
|
59
|
+
<div ref={dialogRef} className="pw-lightbox" role="dialog" aria-modal="true" aria-label={t.imageViewer} onClick={onClose}>
|
|
60
|
+
<div className="pw-lightbox-bar" onClick={(event) => event.stopPropagation()}>
|
|
61
|
+
<span className="pw-lightbox-count">{count > 1 ? `${index + 1} / ${count}` : ""}</span>
|
|
62
|
+
{onRemove && <button type="button" className="pw-lightbox-btn" disabled={disabled} onClick={() => onRemove(index)}>{t.delete}</button>}
|
|
63
|
+
<button ref={closeRef} type="button" className="pw-lightbox-btn" aria-label={t.close} onClick={onClose}>✕</button>
|
|
64
|
+
</div>
|
|
65
|
+
<img src={imageSource(images[index])} alt={t.imageViewer} className="pw-lightbox-image" onClick={(event) => event.stopPropagation()} />
|
|
66
|
+
{count > 1 && <>
|
|
67
|
+
<button type="button" className="pw-lightbox-nav" data-side="prev" aria-label={t.imagePrev} onClick={(event) => { event.stopPropagation(); go(-1); }}>‹</button>
|
|
68
|
+
<button type="button" className="pw-lightbox-nav" data-side="next" aria-label={t.imageNext} onClick={(event) => { event.stopPropagation(); go(1); }}>›</button>
|
|
69
|
+
</>}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|