@rexvane/piweb 0.3.20 → 0.3.22
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 +5 -5
- package/.next/next-minimal-server.js.nft.json +1 -1
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +7 -7
- package/.next/react-loadable-manifest.json +12 -12
- package/.next/required-server-files.js +18 -2
- package/.next/required-server-files.json +18 -2
- package/.next/routes-manifest.json +12 -0
- package/.next/server/app/_global-error/page.js +2 -2
- package/.next/server/app/_global-error/page.js.nft.json +1 -1
- 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.meta +8 -0
- package/.next/server/app/_global-error.rsc +15 -11
- package/.next/server/app/_global-error.segments/_full.segment.rsc +15 -11
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +20 -16
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +3 -1
- package/.next/server/app/_not-found/page.js +1 -1
- package/.next/server/app/_not-found/page.js.nft.json +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.meta +8 -0
- package/.next/server/app/_not-found.rsc +17 -13
- package/.next/server/app/_not-found.segments/_full.segment.rsc +17 -13
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +24 -20
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +4 -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 -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 +5 -0
- package/.next/server/app/api/github/route.js.nft.json +1 -0
- package/.next/server/app/api/github/route_client-reference-manifest.js +1 -0
- 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/health/route.js.nft.json +1 -1
- package/.next/server/app/api/models/defaults/route.js +1 -0
- package/.next/server/app/api/models/defaults/route.js.nft.json +1 -0
- package/.next/server/app/api/models/defaults/route_client-reference-manifest.js +1 -0
- 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 +3 -3
- package/.next/server/app/api/remote/route.js.nft.json +1 -1
- package/.next/server/app/api/security/route.js +1 -1
- package/.next/server/app/api/security/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/import/route.js +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 +1 -1
- package/.next/server/app/api/web-auth/route.js.nft.json +1 -1
- package/.next/server/app/api/workspaces/route.js +1 -1
- package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
- package/.next/server/app/icon.svg/route.js +1 -1
- package/.next/server/app/icon.svg/route.js.nft.json +1 -1
- package/.next/server/app/icon.svg.meta +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.meta +8 -0
- package/.next/server/app/index.rsc +19 -13
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +25 -19
- package/.next/server/app/index.segments/_full.segment.rsc +19 -13
- package/.next/server/app/index.segments/_tree.segment.rsc +3 -2
- package/.next/server/app/login/page.js +1 -1
- package/.next/server/app/login/page.js.nft.json +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.meta +8 -0
- package/.next/server/app/login.rsc +23 -17
- package/.next/server/app/login.segments/_full.segment.rsc +23 -17
- package/.next/server/app/login.segments/_tree.segment.rsc +4 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +30 -24
- 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/421.js +1 -0
- package/.next/server/chunks/430.js +215 -31
- package/.next/server/chunks/496.js +1 -1
- package/.next/server/chunks/538.js +1 -1
- package/.next/server/chunks/662.js +2 -0
- package/.next/server/chunks/686.js +1 -1
- package/.next/server/chunks/807.js +1 -1
- package/.next/server/chunks/813.js +23 -1
- package/.next/server/chunks/920.js +2 -2
- package/.next/server/chunks/924.js +1 -1
- package/.next/server/chunks/970.js +305 -0
- package/.next/server/instrumentation.js +1 -1
- 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/middleware.js +185 -23
- package/.next/server/middleware.js.nft.json +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/preview-props.json +5 -0
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/WMazYvD-gbeLsCUfUxvvM/_buildManifest.js +1 -0
- package/.next/static/chunks/316-698d10eb465bd8ef.js +1 -0
- package/.next/static/chunks/3794-0978865cda773008.js +52 -0
- package/.next/static/chunks/4673.4cf66cbdd8364c57.js +1 -0
- package/.next/static/chunks/4686-09fcd0028079006c.js +1 -0
- package/.next/static/chunks/4778.eddd3a5813f85824.js +1 -0
- package/.next/static/chunks/4bd1b696-e199e788e58acf79.js +1 -0
- package/.next/static/chunks/{5173.91ddb803785009f8.js → 5173.dfd1cbb795b9a21c.js} +1 -1
- package/.next/static/chunks/618.949981da99be7182.js +4 -0
- package/.next/static/chunks/7411.67366dc5032a141d.js +1 -0
- package/.next/static/chunks/8552.25c39ef4c995eba0.js +1 -0
- package/.next/static/chunks/app/_global-error/page-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/events/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/tree/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/agent/new/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/config/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/files/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/fs/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/git/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/github/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/growth/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/health/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/models/defaults/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/models/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/pi-settings/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/plugins/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/prompts/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/remote/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/security/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/export/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/sessions/import/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/sessions/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/skills/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/update/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/version/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/web-auth/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/workspaces/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/page-fc4ed2ff29b6235e.js +1 -0
- package/.next/static/chunks/framework-47b723d27f4a4963.js +1 -0
- package/.next/static/chunks/main-5403f0764579e5fe.js +5 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/webpack-22ebffc20adb44ae.js +1 -0
- package/.next/static/css/6671d0d1589fdc0e.css +1 -0
- package/.next/static/css/{2959bd6b626b7ffe.css → f15a853af8b5127f.css} +1 -1
- package/.next/types/app/api/agent/[id]/events/route.ts +3 -1
- package/.next/types/app/api/agent/[id]/route.ts +3 -1
- package/.next/types/app/api/agent/[id]/tree/route.ts +3 -1
- package/.next/types/app/api/agent/new/route.ts +3 -1
- package/.next/types/app/api/config/route.ts +3 -1
- package/.next/types/app/api/files/route.ts +3 -1
- package/.next/types/app/api/fs/route.ts +3 -1
- package/.next/types/app/api/git/route.ts +3 -1
- package/.next/types/app/api/github/route.ts +355 -0
- package/.next/types/app/api/growth/route.ts +3 -1
- package/.next/types/app/api/health/route.ts +3 -1
- package/.next/types/app/api/models/defaults/route.ts +355 -0
- package/.next/types/app/api/models/route.ts +3 -1
- package/.next/types/app/api/pi-settings/route.ts +3 -1
- package/.next/types/app/api/plugins/route.ts +3 -1
- package/.next/types/app/api/prompts/route.ts +3 -1
- package/.next/types/app/api/remote/route.ts +3 -1
- package/.next/types/app/api/security/route.ts +3 -1
- package/.next/types/app/api/sessions/[id]/export/route.ts +3 -1
- package/.next/types/app/api/sessions/[id]/route.ts +3 -1
- package/.next/types/app/api/sessions/import/route.ts +3 -1
- package/.next/types/app/api/sessions/route.ts +3 -1
- package/.next/types/app/api/skills/route.ts +3 -1
- package/.next/types/app/api/update/route.ts +3 -1
- package/.next/types/app/api/version/route.ts +3 -1
- package/.next/types/app/api/web-auth/route.ts +3 -1
- package/.next/types/app/api/workspaces/route.ts +3 -1
- package/.next/types/app/layout.ts +4 -1
- package/.next/types/app/login/page.ts +4 -1
- package/.next/types/app/page.ts +4 -1
- package/.next/types/cache-life.d.ts +11 -43
- package/.next/types/routes.d.ts +3 -1
- package/.next/types/validator.ts +76 -2
- package/README.md +14 -5
- package/next.config.ts +4 -2
- package/package.json +2 -2
- package/scripts/release.mjs +1 -1
- package/src/app/api/agent/[id]/events/route.ts +42 -13
- package/src/app/api/files/route.ts +5 -2
- package/src/app/api/github/route.ts +26 -0
- package/src/app/api/models/defaults/route.ts +18 -0
- package/src/app/api/models/route.ts +24 -1
- package/src/app/api/update/route.ts +18 -2
- package/src/app/globals.css +275 -0
- package/src/components/AppShell.tsx +89 -33
- package/src/components/ChatInput.tsx +245 -63
- package/src/components/ChatWindow.tsx +123 -108
- package/src/components/ContextMeter.tsx +204 -123
- package/src/components/DrawerPanel.tsx +19 -26
- package/src/components/ExtensionUI.tsx +8 -2
- package/src/components/GitPanel.tsx +110 -40
- package/src/components/MarkdownBlock.tsx +333 -37
- package/src/components/ProjectPanel.tsx +55 -30
- package/src/components/PromptPanel.tsx +2 -4
- package/src/components/ProviderSetupModal.module.css +20 -0
- package/src/components/ProviderSetupModal.tsx +134 -29
- package/src/components/SettingsPanel.tsx +365 -80
- package/src/components/TrajectoryView.tsx +4 -6
- package/src/components/chat/chat-scroll.ts +18 -0
- package/src/components/composer/ComposerImages.tsx +306 -0
- package/src/components/update/CopyCommand.tsx +34 -0
- package/src/components/update/UpdateNotice.tsx +130 -0
- package/src/components/update/update-store.ts +155 -0
- package/src/hooks/useGrowth.ts +3 -0
- package/src/hooks/usePiWeb.ts +54 -27
- package/src/hooks/useWorkspaceFiles.ts +93 -0
- package/src/i18n.tsx +182 -6
- package/src/instrumentation-node.ts +3 -0
- package/src/lib/agent-manager.ts +193 -119
- package/src/lib/chat/event-stream.ts +113 -0
- package/src/lib/chat/stream-settings.ts +192 -0
- package/src/lib/files-service.ts +100 -1
- package/src/lib/format.ts +56 -0
- package/src/lib/git-service.ts +45 -13
- package/src/lib/github-identity.ts +245 -0
- package/src/lib/growth-service.ts +6 -2
- package/src/lib/hero-model-memory.ts +28 -0
- package/src/lib/model-defaults.ts +105 -0
- package/src/lib/model-draft.ts +257 -9
- package/src/lib/model-endpoint.ts +23 -0
- package/src/lib/model-files.ts +33 -0
- package/src/lib/models-service.ts +240 -78
- package/src/lib/pi.ts +27 -2
- package/src/lib/process-format.ts +3 -2
- package/src/lib/session/metrics.ts +31 -0
- package/src/lib/thinking-level.ts +39 -0
- package/src/lib/trajectory.ts +17 -3
- package/src/lib/types.ts +19 -1
- package/src/lib/update/startup-check.ts +117 -0
- package/src/lib/update-service.ts +23 -3
- package/src/lib/workspace-index.ts +270 -0
- package/.next/server/chunks/131.js +0 -1
- package/.next/server/chunks/243.js +0 -1
- package/.next/server/chunks/741.js +0 -165
- package/.next/static/_Y3Lq0lI5RJ1tFSOnDi0L/_buildManifest.js +0 -1
- package/.next/static/chunks/316-5ea443ee6b512aa9.js +0 -1
- package/.next/static/chunks/3794-3679f1c05160977c.js +0 -52
- package/.next/static/chunks/4673.8a5a9e6855350e34.js +0 -1
- package/.next/static/chunks/4686-b1e9fa876d0ea671.js +0 -1
- package/.next/static/chunks/4778.e4c9ed093a413bc4.js +0 -1
- package/.next/static/chunks/4bd1b696-93bf506b236d6248.js +0 -1
- package/.next/static/chunks/618.ee5d5bf1c647e483.js +0 -4
- package/.next/static/chunks/839.d3fe337a9117c325.js +0 -1
- package/.next/static/chunks/8552.6bfe3e101250b20f.js +0 -1
- package/.next/static/chunks/app/_global-error/page-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/events/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/tree/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/agent/new/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/config/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/files/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/fs/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/git/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/growth/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/health/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/models/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/pi-settings/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/plugins/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/prompts/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/remote/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/security/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/export/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/sessions/import/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/skills/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/update/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/version/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/web-auth/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/workspaces/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/page-2e425ef83fba0782.js +0 -1
- package/.next/static/chunks/framework-c4e651fe89edfa91.js +0 -1
- package/.next/static/chunks/main-6159b22a8cb0b7c5.js +0 -5
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/webpack-3614a8337843456b.js +0 -1
- package/.next/static/css/7391c42e1ae617f9.css +0 -1
- /package/.next/static/{_Y3Lq0lI5RJ1tFSOnDi0L → WMazYvD-gbeLsCUfUxvvM}/_ssgManifest.js +0 -0
|
@@ -9,6 +9,7 @@ import { PiMark } from "@/components/PiMark";
|
|
|
9
9
|
import { ChatInput, EMPTY_CHAT_DRAFT, type ChatDraft, type ChatDraftUpdate } from "@/components/ChatInput";
|
|
10
10
|
import { ChatWindow, SessionStatsBar, type RoundBadge } from "@/components/ChatWindow";
|
|
11
11
|
import { ExtensionDialogHost, ExtensionNotices } from "@/components/ExtensionUI";
|
|
12
|
+
import { UpdateNotice } from "@/components/update/UpdateNotice";
|
|
12
13
|
import dynamic from "next/dynamic";
|
|
13
14
|
import { SessionSidebar } from "@/components/SessionSidebar";
|
|
14
15
|
import type { DrawerSection } from "@/components/DrawerPanel";
|
|
@@ -40,6 +41,9 @@ import { useFileViewer } from "@/hooks/useFileViewer";
|
|
|
40
41
|
import type { TreeNode } from "@/lib/growth-tree";
|
|
41
42
|
import { syncPebrelTheme } from "@/lib/theme";
|
|
42
43
|
import type { ModelChoice } from "@/components/ModelSelector";
|
|
44
|
+
import type { NewSessionDefaults } from "@/lib/model-defaults";
|
|
45
|
+
import { readRememberedHeroModel, writeRememberedHeroModel } from "@/lib/hero-model-memory";
|
|
46
|
+
import { newSessionThinkingLevel } from "@/lib/thinking-level";
|
|
43
47
|
import type { ImageAttachment, TrajEntry } from "@/lib/types";
|
|
44
48
|
|
|
45
49
|
// dsh ui-layout columns.ts 几何常量
|
|
@@ -163,6 +167,8 @@ export function AppShell() {
|
|
|
163
167
|
const growthSelectLatest = useRef<() => void>(() => {});
|
|
164
168
|
const [heroCwd, setHeroCwd] = useState("");
|
|
165
169
|
const [heroModel, setHeroModel] = useState<{ provider: string; id: string } | null>(null);
|
|
170
|
+
/** 当前工作区的新会话默认值(pi 的初始模型规则 + 设置里的思考档位),切换工作区时重新取 */
|
|
171
|
+
const [heroDefaults, setHeroDefaults] = useState<NewSessionDefaults | null>(null);
|
|
166
172
|
const [heroThinking, setHeroThinking] = useState("");
|
|
167
173
|
const [isNarrow, setIsNarrow] = useState(false);
|
|
168
174
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
|
@@ -172,6 +178,29 @@ export function AppShell() {
|
|
|
172
178
|
setSidebarCollapsed(localStorage.getItem("piweb.sidebarCollapsed") === "1");
|
|
173
179
|
setDrawerOpen(localStorage.getItem("piweb.drawerOpen") === "1");
|
|
174
180
|
}, []);
|
|
181
|
+
/** 新会话时加载用户之前的选择,或使用系统建议(第一个认证的) */
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (!heroCwd) {
|
|
184
|
+
setHeroModel(null);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
// 只认这个工作区自己记住的模型;没有就不预选,输入框等用户在新会话页选
|
|
188
|
+
setHeroModel(readRememberedHeroModel(localStorage, heroCwd));
|
|
189
|
+
}, [heroCwd]);
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
setHeroDefaults(null);
|
|
192
|
+
if (!heroCwd) return;
|
|
193
|
+
let stale = false;
|
|
194
|
+
fetch(`/api/models/defaults?cwd=${encodeURIComponent(heroCwd)}`)
|
|
195
|
+
.then((r) => r.json() as Promise<{ success?: boolean; data?: NewSessionDefaults }>)
|
|
196
|
+
.then((json) => {
|
|
197
|
+
if (!stale && json.success && json.data) setHeroDefaults(json.data);
|
|
198
|
+
})
|
|
199
|
+
.catch(() => undefined);
|
|
200
|
+
return () => {
|
|
201
|
+
stale = true;
|
|
202
|
+
};
|
|
203
|
+
}, [heroCwd]);
|
|
175
204
|
/** 右侧抽屉开关(项目栏降级成抽屉入口后,它是"最右一列是否打开"唯一的开关) */
|
|
176
205
|
const toggleDrawer = useCallback((next?: boolean) => {
|
|
177
206
|
setDrawerOpen((cur) => {
|
|
@@ -279,11 +308,28 @@ export function AppShell() {
|
|
|
279
308
|
})),
|
|
280
309
|
[models],
|
|
281
310
|
);
|
|
282
|
-
|
|
311
|
+
// 新会话页当前选中的模型:只有在这个工作区选过(或记住过)才有;没有就是 undefined,输入框保持禁用
|
|
312
|
+
const heroChoice = useMemo(
|
|
313
|
+
() => (heroModel ? modelChoices.find((m) => m.provider === heroModel.provider && m.id === heroModel.id) : undefined),
|
|
314
|
+
[heroModel, modelChoices],
|
|
315
|
+
);
|
|
283
316
|
const heroModelLevels = useMemo(
|
|
284
|
-
() =>
|
|
285
|
-
[
|
|
317
|
+
() => heroChoice?.thinkingLevels ?? ["off", "minimal", "low", "medium", "high", "xhigh", "max"],
|
|
318
|
+
[heroChoice],
|
|
286
319
|
);
|
|
320
|
+
/**
|
|
321
|
+
* 新会话实际会用的思考档位:用户显式选过才算选择(下发时才带上);界面平时只显示这里算出的生效值,
|
|
322
|
+
* 不把它当作选择发给服务端。
|
|
323
|
+
*/
|
|
324
|
+
const heroEffectiveThinking = useMemo(() => {
|
|
325
|
+
const choice = heroChoice;
|
|
326
|
+
return newSessionThinkingLevel({
|
|
327
|
+
supported: heroModelLevels,
|
|
328
|
+
requested: heroThinking,
|
|
329
|
+
modelOverride: choice ? heroDefaults?.modelThinkingLevels[`${choice.provider}/${choice.id}`] : undefined,
|
|
330
|
+
defaultLevel: heroDefaults?.defaultThinkingLevel,
|
|
331
|
+
});
|
|
332
|
+
}, [heroChoice, heroModelLevels, heroThinking, heroDefaults]);
|
|
287
333
|
|
|
288
334
|
|
|
289
335
|
// ---------- 拖拽 ----------
|
|
@@ -461,12 +507,14 @@ export function AppShell() {
|
|
|
461
507
|
setError(t.pickWorkspaceFirst);
|
|
462
508
|
return { success: false };
|
|
463
509
|
}
|
|
464
|
-
//
|
|
465
|
-
|
|
466
|
-
|
|
510
|
+
// 没选模型不建会话:输入框本来就禁用,这里再拦一次;模型一律显式下发,不让 SDK 自选
|
|
511
|
+
if (!heroChoice) {
|
|
512
|
+
setError(t.blockedComposer);
|
|
513
|
+
return { success: false };
|
|
514
|
+
}
|
|
467
515
|
const p = await newSession(cwd, {
|
|
468
|
-
provider:
|
|
469
|
-
modelId:
|
|
516
|
+
provider: heroChoice.provider,
|
|
517
|
+
modelId: heroChoice.id,
|
|
470
518
|
thinking: heroThinking || undefined,
|
|
471
519
|
});
|
|
472
520
|
if (!p) return { success: false };
|
|
@@ -518,7 +566,7 @@ export function AppShell() {
|
|
|
518
566
|
void sendCommand({ cmd: "fork" }).then((r) => {
|
|
519
567
|
if (r.success && r.data?.sessionPath) openSession(r.data.sessionPath);
|
|
520
568
|
});
|
|
521
|
-
} else if (name === "reload") void sendCommand({ cmd: "reload" });
|
|
569
|
+
} else if (name === "reload") void sendCommand({ cmd: "reload" }).then(() => refreshModels());
|
|
522
570
|
else setError(t.cmdUnknown.replace("{name}", name));
|
|
523
571
|
},
|
|
524
572
|
[sendCommand, currentId, closeSession, openSession, setError, t],
|
|
@@ -587,7 +635,9 @@ export function AppShell() {
|
|
|
587
635
|
if (!path) return;
|
|
588
636
|
updateDraft(draftKey, (previous) => {
|
|
589
637
|
const references = previous.references ?? [];
|
|
590
|
-
|
|
638
|
+
// 目录在 @ 选择器里存成 `src/lib/`:去重按去掉尾斜杠比较,避免同一目录两条
|
|
639
|
+
const samePath = (a: string, b: string) => a.replace(/[\\/]+$/, "") === b.replace(/[\\/]+$/, "");
|
|
640
|
+
if (references.some((existing) => samePath(existing, path))) return previous;
|
|
591
641
|
return { ...previous, references: [...references, path] };
|
|
592
642
|
});
|
|
593
643
|
requestAnimationFrame(() => document.querySelector<HTMLTextAreaElement>('[data-testid="composer"] textarea')?.focus());
|
|
@@ -726,7 +776,7 @@ export function AppShell() {
|
|
|
726
776
|
|
|
727
777
|
return (
|
|
728
778
|
<div
|
|
729
|
-
className="grid
|
|
779
|
+
className="pw-app-shell grid w-screen overflow-hidden"
|
|
730
780
|
style={{
|
|
731
781
|
gridTemplateColumns: gridCols,
|
|
732
782
|
// 收起/展开不做过渡:grid-template-columns 是布局属性,动画它会让浏览器逐帧
|
|
@@ -822,15 +872,17 @@ export function AppShell() {
|
|
|
822
872
|
providerNames={providerNames}
|
|
823
873
|
authByProvider={authByProvider}
|
|
824
874
|
addWorkspaceByPicker={addWorkspaceByPicker}
|
|
825
|
-
|
|
875
|
+
heroChoice={heroChoice}
|
|
826
876
|
onSelectHeroModel={(provider, id) => {
|
|
827
|
-
|
|
877
|
+
const choice = { provider, id };
|
|
878
|
+
setHeroModel(choice);
|
|
879
|
+
// 只记在当前工作区名下
|
|
880
|
+
if (heroCwd) writeRememberedHeroModel(localStorage, heroCwd, choice);
|
|
828
881
|
const levels = modelChoices.find((model) => model.provider === provider && model.id === id)?.thinkingLevels ?? [];
|
|
829
882
|
if (heroThinking && !levels.includes(heroThinking)) setHeroThinking("");
|
|
830
883
|
}}
|
|
831
|
-
defaultModel={defaultModel}
|
|
832
884
|
heroModelLevels={heroModelLevels}
|
|
833
|
-
|
|
885
|
+
heroEffectiveThinking={heroEffectiveThinking}
|
|
834
886
|
onSelectHeroThinking={setHeroThinking}
|
|
835
887
|
/>
|
|
836
888
|
{/* 草稿阶段的错误行内显示(不再弹右下角) */}
|
|
@@ -951,7 +1003,7 @@ export function AppShell() {
|
|
|
951
1003
|
contextPercent={snapshot?.contextUsage?.percent ?? null}
|
|
952
1004
|
contextTokens={snapshot?.contextUsage?.tokens ?? null}
|
|
953
1005
|
contextWindow={snapshot?.contextUsage?.contextWindow ?? null}
|
|
954
|
-
contextSource={{ systemChars: contextSystemChars, messages: state.messages, breakdown: snapshot?.contextBreakdown }}
|
|
1006
|
+
contextSource={{ systemChars: contextSystemChars, messages: state.messages, breakdown: snapshot?.contextBreakdown, loadFull: () => sendCommand({ cmd: "prepare" }) }}
|
|
955
1007
|
contextVisible={(snapshot?.messages?.length ?? 0) > 0}
|
|
956
1008
|
model={snapshot?.model}
|
|
957
1009
|
thinkingLevel={snapshot?.thinkingLevel}
|
|
@@ -961,6 +1013,7 @@ export function AppShell() {
|
|
|
961
1013
|
authByProvider={authByProvider}
|
|
962
1014
|
queue={snapshot?.queue ?? { steering: [], followUp: [] }}
|
|
963
1015
|
commands={slashCommands}
|
|
1016
|
+
cwd={panelCwd}
|
|
964
1017
|
onCommand={runSlashCommand}
|
|
965
1018
|
onSend={sendPrompt}
|
|
966
1019
|
onSteer={(text, images) => sendCommand({ cmd: "prompt", text, images, behavior: "steer" })}
|
|
@@ -1037,7 +1090,10 @@ export function AppShell() {
|
|
|
1037
1090
|
)}
|
|
1038
1091
|
{/* 只有远程设备(手机走 tailnet)会走到这里:本机仍然用系统原生对话框 */}
|
|
1039
1092
|
<DirectoryPicker open={dirPickerOpen} onConfirm={(path) => void confirmDirPicker(path)} onCancel={cancelDirPicker} />
|
|
1040
|
-
<ExtensionNotices notices={state.extensionNotices} onDismiss={dismissExtensionNotice}
|
|
1093
|
+
<ExtensionNotices notices={state.extensionNotices} onDismiss={dismissExtensionNotice}>
|
|
1094
|
+
{/* 每次启动提醒一次有没有新版本(PiWeb / pi),和扩展通知摞在同一处 */}
|
|
1095
|
+
<UpdateNotice />
|
|
1096
|
+
</ExtensionNotices>
|
|
1041
1097
|
<SettingsPanel
|
|
1042
1098
|
open={settingsOpen}
|
|
1043
1099
|
onClose={() => {
|
|
@@ -1109,11 +1165,10 @@ function Hero({
|
|
|
1109
1165
|
providerNames,
|
|
1110
1166
|
authByProvider,
|
|
1111
1167
|
addWorkspaceByPicker,
|
|
1112
|
-
|
|
1168
|
+
heroChoice,
|
|
1113
1169
|
onSelectHeroModel,
|
|
1114
|
-
defaultModel,
|
|
1115
1170
|
heroModelLevels,
|
|
1116
|
-
|
|
1171
|
+
heroEffectiveThinking,
|
|
1117
1172
|
onSelectHeroThinking,
|
|
1118
1173
|
}: {
|
|
1119
1174
|
draft: ChatDraft;
|
|
@@ -1135,11 +1190,10 @@ function Hero({
|
|
|
1135
1190
|
providerNames: Record<string, string>;
|
|
1136
1191
|
authByProvider: Record<string, boolean>;
|
|
1137
1192
|
addWorkspaceByPicker: () => Promise<string | null>;
|
|
1138
|
-
|
|
1193
|
+
heroChoice?: ModelChoice;
|
|
1139
1194
|
onSelectHeroModel: (provider: string, id: string) => void;
|
|
1140
|
-
defaultModel?: ModelChoice;
|
|
1141
1195
|
heroModelLevels: string[];
|
|
1142
|
-
|
|
1196
|
+
heroEffectiveThinking: string;
|
|
1143
1197
|
onSelectHeroThinking: (level: string) => void;
|
|
1144
1198
|
}) {
|
|
1145
1199
|
const { t } = useI18n();
|
|
@@ -1233,14 +1287,16 @@ function Hero({
|
|
|
1233
1287
|
|
|
1234
1288
|
{/* 输入卡 */}
|
|
1235
1289
|
<ChatInput
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1290
|
+
draft={draft}
|
|
1291
|
+
onDraftChange={onDraftChange}
|
|
1292
|
+
onUploadError={onUploadError}
|
|
1293
|
+
onUploadProgress={onUploadProgress}
|
|
1294
|
+
uploadFailure={uploadFailure}
|
|
1295
|
+
pendingUploadCount={pendingUploadCount}
|
|
1296
|
+
pendingSend={pendingSend}
|
|
1297
|
+
onSendPendingChange={onSendPendingChange}
|
|
1298
|
+
// 新会话也能 @ 引用:候选来自上面选中的工作区
|
|
1299
|
+
cwd={cwd}
|
|
1244
1300
|
commands={commands}
|
|
1245
1301
|
onCommand={onCommand}
|
|
1246
1302
|
isStreaming={false}
|
|
@@ -1248,8 +1304,8 @@ function Hero({
|
|
|
1248
1304
|
contextTokens={null}
|
|
1249
1305
|
contextWindow={null}
|
|
1250
1306
|
contextVisible={false}
|
|
1251
|
-
model={
|
|
1252
|
-
thinkingLevel={
|
|
1307
|
+
model={heroChoice}
|
|
1308
|
+
thinkingLevel={heroEffectiveThinking}
|
|
1253
1309
|
thinkingLevels={heroModelLevels}
|
|
1254
1310
|
models={models}
|
|
1255
1311
|
providerNames={providerNames}
|
|
@@ -6,18 +6,23 @@
|
|
|
6
6
|
* 运行中:Enter 按设置插话 / 排队,Ctrl(⌘)+Enter 用另一种;停止按钮会把排队消息取回输入框(pi 终端同款);
|
|
7
7
|
* 排队中的消息列在输入卡上方,↑(空输入框时)取回编辑。Enter 发送 / Shift+Enter 换行。
|
|
8
8
|
*/
|
|
9
|
-
import { useEffect, useRef, useState } from "react";
|
|
9
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
10
10
|
import {
|
|
11
11
|
IconAtOutline16,
|
|
12
12
|
IconFileOutline16,
|
|
13
|
+
IconFolderClose16,
|
|
13
14
|
IconSendArrowUp14,
|
|
14
15
|
IconStopFill16,
|
|
15
16
|
IconTerminalOutline14,
|
|
16
17
|
} from "@/components/icons";
|
|
17
18
|
import { ContextMeter } from "@/components/ContextMeter";
|
|
19
|
+
import { COLLAPSE_AT, ComposerImages } from "@/components/composer/ComposerImages";
|
|
18
20
|
import { ModelSelector, type ModelChoice } from "@/components/ModelSelector";
|
|
19
21
|
import { useI18n } from "@/i18n";
|
|
22
|
+
import { useWorkspaceFiles } from "@/hooks/useWorkspaceFiles";
|
|
23
|
+
import { fmtBytes } from "@/lib/format";
|
|
20
24
|
import { otherBehavior, useEnterBehavior } from "@/lib/enter-behavior";
|
|
25
|
+
import { atToken, candidateText, suggestPaths, type PathCandidate } from "@/lib/workspace-index";
|
|
21
26
|
import type { ImageAttachment } from "@/lib/types";
|
|
22
27
|
|
|
23
28
|
export interface SlashCommand {
|
|
@@ -55,6 +60,7 @@ export function ChatInput({
|
|
|
55
60
|
contextWindow,
|
|
56
61
|
contextSource,
|
|
57
62
|
contextVisible = true,
|
|
63
|
+
cwd,
|
|
58
64
|
model,
|
|
59
65
|
thinkingLevel,
|
|
60
66
|
thinkingLevels,
|
|
@@ -89,11 +95,14 @@ export function ChatInput({
|
|
|
89
95
|
/** 上下文分段数据源:ContextMeter 弹窗打开时才做字符统计 */
|
|
90
96
|
contextSource?: {
|
|
91
97
|
systemChars: number;
|
|
92
|
-
messages: { role?: string; content: Array<{ type: string; text?: string; thinking?: string }
|
|
98
|
+
messages: { role?: string; content: Array<{ type: string; text?: string; thinking?: string; arguments?: unknown; name?: string }>; usage?: import("@/lib/types").TrajTokens; stopReason?: string }[];
|
|
93
99
|
breakdown?: import("@/lib/types").ContextBreakdown;
|
|
100
|
+
loadFull?: () => Promise<unknown> | unknown;
|
|
94
101
|
};
|
|
95
102
|
/** 还没有对话时隐藏上下文计量 */
|
|
96
103
|
contextVisible?: boolean;
|
|
104
|
+
/** 工作区根:@ 引用的候选清单从这里取(不传就不启用 @ 引用) */
|
|
105
|
+
cwd?: string;
|
|
97
106
|
model?: { provider: string; id: string; name: string };
|
|
98
107
|
thinkingLevel?: string;
|
|
99
108
|
thinkingLevels: string[];
|
|
@@ -148,10 +157,18 @@ export function ChatInput({
|
|
|
148
157
|
const uploadBusy = uploadCount > 0 || pendingUploadCount > 0;
|
|
149
158
|
const sendBusy = sending || pendingSend;
|
|
150
159
|
const [dragActive, setDragActive] = useState(false);
|
|
160
|
+
/** 引用 chip 超过 COLLAPSE_AT 就先折叠:手机上折行会把输入框顶掉(图片的折叠在 ComposerImages 里) */
|
|
161
|
+
const [refsExpanded, setRefsExpanded] = useState(false);
|
|
151
162
|
const dragDepth = useRef(0);
|
|
152
163
|
const [modelOpen, setModelOpen] = useState(false);
|
|
153
164
|
const [cmdIdx, setCmdIdx] = useState(0);
|
|
154
165
|
const [cmdDismissed, setCmdDismissed] = useState(false);
|
|
166
|
+
// @ 引用:打字触发候选,选中后落成引用 chip(发送时统一拼成 `@路径` 行)
|
|
167
|
+
const [mention, setMention] = useState<{ query: string; start: number; tokenEnd: number } | null>(null);
|
|
168
|
+
const [mentionIdx, setMentionIdx] = useState(0);
|
|
169
|
+
/** 被 Esc 关掉的那个 token(位置 + 原文);光标在它里面移动不再弹,改了字、或换成别的 token 重新弹 */
|
|
170
|
+
const mentionDismissed = useRef<string | null>(null);
|
|
171
|
+
const mentionMenuRef = useRef<HTMLDivElement>(null);
|
|
155
172
|
const taRef = useRef<HTMLTextAreaElement>(null);
|
|
156
173
|
const wrapRef = useRef<HTMLDivElement>(null);
|
|
157
174
|
const { t } = useI18n();
|
|
@@ -178,6 +195,79 @@ export function ChatInput({
|
|
|
178
195
|
const filteredCommands = (commands ?? []).filter((c) => c.name.toLowerCase().startsWith(slash.split(" ")[0].toLowerCase()));
|
|
179
196
|
const visibleCommands = filteredCommands;
|
|
180
197
|
const commandMode = typedCommandMode;
|
|
198
|
+
|
|
199
|
+
// ---------- @ 引用:输入 @ 弹候选,选中落成引用 chip ----------
|
|
200
|
+
// 候选清单一次拉全量(工作区 git 可见文件),之后每个字符都在本地匹配;规则与 pi 终端的 @ 补全一致(见 workspace-index)。
|
|
201
|
+
const mentionMode = mention !== null;
|
|
202
|
+
const mentionQuery = mention?.query ?? null;
|
|
203
|
+
const files = useWorkspaceFiles(cwd ?? "", mentionMode);
|
|
204
|
+
// 只在候选清单或查询变了时重算:光标移动、其它状态变化引起的重渲染不该每次扫一遍全量清单
|
|
205
|
+
const suggestions = useMemo(
|
|
206
|
+
() => (mentionQuery === null ? [] : suggestPaths(files.candidates, mentionQuery)),
|
|
207
|
+
[files.candidates, mentionQuery],
|
|
208
|
+
);
|
|
209
|
+
const effectiveMentionIdx = Math.min(mentionIdx, Math.max(0, suggestions.length - 1));
|
|
210
|
+
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
mentionMenuRef.current?.querySelector('[aria-selected="true"]')?.scrollIntoView({ block: "nearest" });
|
|
213
|
+
}, [mentionIdx, suggestions.length]);
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 文本 / 光标变了就重新判是否在 @ token 里(Esc 关过的同一个 token 不再弹)。
|
|
217
|
+
* 没有工作区就不弹:没有候选来源,菜单只会空转还拦着方向键和 Esc。
|
|
218
|
+
*/
|
|
219
|
+
const syncMention = (value: string, caret: number | null) => {
|
|
220
|
+
const token = caret === null || !cwd ? null : atToken(value, caret);
|
|
221
|
+
if (!token) {
|
|
222
|
+
// 离开 token 就忘掉 Esc:之后在同一位置打新的 @ 照常弹
|
|
223
|
+
mentionDismissed.current = null;
|
|
224
|
+
setMention(null);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (mentionDismissed.current === `${token.start}:${value.slice(token.start, token.tokenEnd)}`) {
|
|
228
|
+
setMention(null);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (!mention || mention.start !== token.start || mention.query !== token.query) setMentionIdx(0);
|
|
232
|
+
if (!mention || mention.start !== token.start || mention.query !== token.query || mention.tokenEnd !== token.tokenEnd) {
|
|
233
|
+
setMention({ query: token.query, start: token.start, tokenEnd: token.tokenEnd });
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const closeMention = (dismiss: boolean) => {
|
|
238
|
+
if (dismiss && mention) mentionDismissed.current = `${mention.start}:${text.slice(mention.start, mention.tokenEnd)}`;
|
|
239
|
+
setMention(null);
|
|
240
|
+
setMentionIdx(0);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/** 选中候选:目录引用文件夹本身(要往下钻就继续打 `@src/lib/`),token 从正文里播掉 */
|
|
244
|
+
const applyMention = (candidate: PathCandidate) => {
|
|
245
|
+
if (!mention) return;
|
|
246
|
+
const { start, tokenEnd } = mention;
|
|
247
|
+
// 目录存成 `src/lib/`:chip 靠尾斜杠判断文件夹(去重会把尾斜杠归一化掉)
|
|
248
|
+
const path = candidate.dir ? `${candidate.path}/` : candidate.path;
|
|
249
|
+
changeDraft((previous) => {
|
|
250
|
+
const refs = previous.references ?? [];
|
|
251
|
+
// 目录带尾斜杠、文件不带:去重按去掉尾斜杠比较,
|
|
252
|
+
// 否则「文件面板引用的 src/lib」与「@ 选的 src/lib/」会变成两个 chip
|
|
253
|
+
const samePath = (a: string, b: string) => a.replace(/[\\/]+$/, "") === b.replace(/[\\/]+$/, "");
|
|
254
|
+
return {
|
|
255
|
+
...previous,
|
|
256
|
+
text: previous.text.slice(0, start) + previous.text.slice(tokenEnd),
|
|
257
|
+
references: refs.some((existing) => samePath(existing, path)) ? refs : [...refs, path],
|
|
258
|
+
};
|
|
259
|
+
});
|
|
260
|
+
setMention(null);
|
|
261
|
+
setMentionIdx(0);
|
|
262
|
+
requestAnimationFrame(() => {
|
|
263
|
+
autoSize();
|
|
264
|
+
const ta = taRef.current;
|
|
265
|
+
if (ta) {
|
|
266
|
+
ta.focus();
|
|
267
|
+
ta.setSelectionRange(start, start);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
};
|
|
181
271
|
// 命令菜单滚动:.popover 的 overflow:hidden 会压掉 overflow-y-auto(unlayered CSS 优先),
|
|
182
272
|
// 内联 overflowY 强制可滚;键盘导航时把选中项滚进视野
|
|
183
273
|
const cmdMenuRef = useRef<HTMLDivElement>(null);
|
|
@@ -444,6 +534,63 @@ export function ChatInput({
|
|
|
444
534
|
{attachmentError}
|
|
445
535
|
</div>
|
|
446
536
|
)}
|
|
537
|
+
{/* @ 引用候选(与斜杠菜单同一处锚点:输入卡上方) */}
|
|
538
|
+
{mentionMode && (
|
|
539
|
+
<div
|
|
540
|
+
ref={mentionMenuRef}
|
|
541
|
+
data-testid="mention-menu"
|
|
542
|
+
className="popover absolute bottom-full left-0 right-0 z-50 mb-2 max-h-[340px] overflow-y-auto py-1.5"
|
|
543
|
+
role="listbox"
|
|
544
|
+
aria-label={t.mentionFiles}
|
|
545
|
+
style={{ overflowY: "auto" }}
|
|
546
|
+
>
|
|
547
|
+
<div className="flex items-center gap-2 px-4 pb-1 pt-1" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
|
|
548
|
+
<span>{t.mentionFiles}</span>
|
|
549
|
+
{files.truncated && <span style={{ color: "var(--dsw-warn)" }}>{t.mentionTruncated}</span>}
|
|
550
|
+
{files.source === "disk" && <span title={t.mentionDiskSourceHint}>{t.mentionDiskSource}</span>}
|
|
551
|
+
<span className="flex-1" />
|
|
552
|
+
<span>{t.mentionHint}</span>
|
|
553
|
+
</div>
|
|
554
|
+
{files.status === "loading" && (
|
|
555
|
+
<div className="px-4 py-2" style={{ fontSize: 12.5, color: "var(--dsw-label-caption)" }}>{t.panelLoading}</div>
|
|
556
|
+
)}
|
|
557
|
+
{files.status === "error" && (
|
|
558
|
+
<div className="px-4 py-2" style={{ fontSize: 12.5, color: "var(--dsw-danger)" }} role="alert">{files.error}</div>
|
|
559
|
+
)}
|
|
560
|
+
{files.status === "ready" && suggestions.length === 0 && (
|
|
561
|
+
<div className="px-4 py-2" style={{ fontSize: 12.5, color: "var(--dsw-label-caption)" }}>{t.mentionNoMatch}</div>
|
|
562
|
+
)}
|
|
563
|
+
{suggestions.map(({ candidate, label, description }, i) => (
|
|
564
|
+
<button
|
|
565
|
+
key={`${candidate.dir ? "d" : "f"}:${candidate.path}`}
|
|
566
|
+
type="button"
|
|
567
|
+
role="option"
|
|
568
|
+
aria-selected={i === effectiveMentionIdx}
|
|
569
|
+
className="flex w-full items-center gap-2 px-4 py-1.5 text-left transition-colors"
|
|
570
|
+
style={{ background: i === effectiveMentionIdx ? "var(--dsw-hover)" : "transparent" }}
|
|
571
|
+
onMouseEnter={() => setMentionIdx(i)}
|
|
572
|
+
onClick={() => applyMention(candidate)}
|
|
573
|
+
title={candidateText(candidate)}
|
|
574
|
+
>
|
|
575
|
+
{candidate.dir ? (
|
|
576
|
+
<IconFolderClose16 size={13} style={{ flex: "none", color: "var(--dsw-warn)" }} />
|
|
577
|
+
) : (
|
|
578
|
+
<IconFileOutline16 size={13} style={{ flex: "none", color: "var(--dsw-label-caption)" }} />
|
|
579
|
+
)}
|
|
580
|
+
{/* 和 pi 终端一样:名字在前,路径在后;根目录下的条目两者相同,不重复显示 */}
|
|
581
|
+
<span className="flex-none truncate" style={{ maxWidth: "55%", fontSize: 12.5, fontFamily: "var(--font-mono)", color: "var(--dsw-label-primary)" }}>
|
|
582
|
+
{label}
|
|
583
|
+
</span>
|
|
584
|
+
{description.replace(/\/$/, "") !== label.replace(/\/$/, "") && (
|
|
585
|
+
<span className="min-w-0 flex-1 truncate" style={{ fontSize: 11.5, fontFamily: "var(--font-mono)", color: "var(--dsw-label-caption)" }}>
|
|
586
|
+
{description}
|
|
587
|
+
</span>
|
|
588
|
+
)}
|
|
589
|
+
</button>
|
|
590
|
+
))}
|
|
591
|
+
</div>
|
|
592
|
+
)}
|
|
593
|
+
|
|
447
594
|
{/* 斜杠命令菜单(dsh:悬浮于输入卡上方,同宽) */}
|
|
448
595
|
{commandMode && visibleCommands.length > 0 && (
|
|
449
596
|
<div ref={cmdMenuRef} className="popover absolute bottom-full left-0 right-0 z-50 mb-2 max-h-[340px] overflow-y-auto py-1.5" role="listbox" aria-label={t.slashCatalog} style={{ overflowY: "auto" }}>
|
|
@@ -477,65 +624,62 @@ export function ChatInput({
|
|
|
477
624
|
</div>
|
|
478
625
|
)}
|
|
479
626
|
|
|
480
|
-
{/*
|
|
627
|
+
{/* 附件轨:图片(超过 4 张折叠成牌堆,展开沿弧线扇开;点开可放大、切换、删除) */}
|
|
481
628
|
{images.length > 0 && (
|
|
482
|
-
<
|
|
483
|
-
{images
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
alt=""
|
|
488
|
-
className="h-14 w-14 rounded-lg object-cover"
|
|
489
|
-
style={{ border: "0.5px solid var(--dsw-border-l2)" }}
|
|
490
|
-
/>
|
|
491
|
-
<button
|
|
492
|
-
className="absolute -right-1.5 -top-1.5 flex h-5 w-5 items-center justify-center rounded-full text-[10px]"
|
|
493
|
-
style={{ background: "var(--dsw-label-primary)", color: "var(--dsw-bg-base)" }}
|
|
494
|
-
disabled={sendBusy}
|
|
495
|
-
onClick={() => changeDraft((previous) => ({ ...previous, images: previous.images.filter((_, index) => index !== i) }))}
|
|
496
|
-
>
|
|
497
|
-
✕
|
|
498
|
-
</button>
|
|
499
|
-
</div>
|
|
500
|
-
))}
|
|
501
|
-
</div>
|
|
629
|
+
<ComposerImages
|
|
630
|
+
images={images}
|
|
631
|
+
disabled={sendBusy}
|
|
632
|
+
onRemove={(index) => changeDraft((previous) => ({ ...previous, images: previous.images.filter((_, at) => at !== index) }))}
|
|
633
|
+
/>
|
|
502
634
|
)}
|
|
503
635
|
|
|
504
|
-
{/* 附件轨:上传的普通文件(压缩包/文档等,路径引用) */}
|
|
505
636
|
{/* 路径引用(项目结构图/文件面板的「引用到输入框」):独立于正文,发送时拼成 @路径 */}
|
|
506
637
|
{references.length > 0 && (
|
|
507
638
|
<div className="mb-2 flex flex-wrap gap-1.5 px-1" data-testid="composer-references">
|
|
508
|
-
{references.map((path) =>
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
<span className="truncate">{referenceLabelOf(path)}</span>
|
|
524
|
-
<button
|
|
525
|
-
type="button"
|
|
526
|
-
className="flex h-4 w-4 flex-none items-center justify-center rounded-full text-[10px]"
|
|
527
|
-
style={{ color: "var(--dsw-label-caption)" }}
|
|
528
|
-
disabled={sendBusy}
|
|
529
|
-
onClick={() => changeDraft((previous) => ({ ...previous, references: (previous.references ?? []).filter((p) => p !== path) }))}
|
|
530
|
-
aria-label={`${t.delete} ${path}`}
|
|
639
|
+
{(refsExpanded ? references : references.slice(0, COLLAPSE_AT)).map((path) => {
|
|
640
|
+
const chip = referenceChipOf(path);
|
|
641
|
+
return (
|
|
642
|
+
<span
|
|
643
|
+
key={path}
|
|
644
|
+
className="flex items-center gap-1 rounded-md px-2 py-1"
|
|
645
|
+
style={{
|
|
646
|
+
maxWidth: 260,
|
|
647
|
+
fontSize: 12,
|
|
648
|
+
fontFamily: "var(--font-mono)",
|
|
649
|
+
color: "var(--dsw-label-secondary)",
|
|
650
|
+
background: "var(--dsw-selector)",
|
|
651
|
+
border: "0.5px solid var(--dsw-border-l3)",
|
|
652
|
+
}}
|
|
653
|
+
title={path}
|
|
531
654
|
>
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
655
|
+
{chip.dir ? (
|
|
656
|
+
<IconFolderClose16 size={12} style={{ flex: "none", color: "var(--dsw-warn)" }} />
|
|
657
|
+
) : (
|
|
658
|
+
<IconFileOutline16 size={12} style={{ flex: "none", color: "var(--dsw-accent)" }} />
|
|
659
|
+
)}
|
|
660
|
+
<span className="truncate">{chip.text}</span>
|
|
661
|
+
<button
|
|
662
|
+
type="button"
|
|
663
|
+
className="flex h-4 w-4 flex-none items-center justify-center rounded-full text-[10px]"
|
|
664
|
+
style={{ color: "var(--dsw-label-caption)" }}
|
|
665
|
+
disabled={sendBusy}
|
|
666
|
+
onClick={() => changeDraft((previous) => ({ ...previous, references: (previous.references ?? []).filter((p) => p !== path) }))}
|
|
667
|
+
aria-label={`${t.delete} ${path}`}
|
|
668
|
+
>
|
|
669
|
+
✕
|
|
670
|
+
</button>
|
|
671
|
+
</span>
|
|
672
|
+
);
|
|
673
|
+
})}
|
|
674
|
+
{references.length > COLLAPSE_AT ? (
|
|
675
|
+
<button type="button" className="pw-chip" data-testid="references-toggle" onClick={() => setRefsExpanded((open) => !open)}>
|
|
676
|
+
{refsExpanded ? t.chipCollapse : t.chipExpandMore.replace("{n}", String(references.length - COLLAPSE_AT))}
|
|
677
|
+
</button>
|
|
678
|
+
) : null}
|
|
536
679
|
</div>
|
|
537
680
|
)}
|
|
538
681
|
|
|
682
|
+
{/* 附件轨:上传的普通文件(压缩包/文档等,路径引用) */}
|
|
539
683
|
{uploads.length > 0 && (
|
|
540
684
|
<div className="mb-2 flex flex-wrap gap-2 px-1">
|
|
541
685
|
{uploads.map((f, i) => (
|
|
@@ -604,12 +748,19 @@ export function ChatInput({
|
|
|
604
748
|
suppressHydrationWarning
|
|
605
749
|
className="min-w-0 flex-1 resize-none"
|
|
606
750
|
style={{ fontSize: "var(--piweb-chat-font-size, var(--dsh-content-font-size))", lineHeight: 1.55 }}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
751
|
+
onChange={(e) => {
|
|
752
|
+
const value = e.target.value;
|
|
753
|
+
setText(value);
|
|
754
|
+
setCmdDismissed(false);
|
|
610
755
|
setCmdIdx(0);
|
|
756
|
+
syncMention(value, e.target.selectionStart);
|
|
611
757
|
autoSize();
|
|
612
758
|
}}
|
|
759
|
+
onSelect={(e) => {
|
|
760
|
+
// 选中了一段文字时不弹:那时 Enter / Tab 应该照常工作
|
|
761
|
+
const ta = e.target as HTMLTextAreaElement;
|
|
762
|
+
syncMention(ta.value, ta.selectionStart === ta.selectionEnd ? ta.selectionStart : null);
|
|
763
|
+
}}
|
|
613
764
|
onPaste={(e) => {
|
|
614
765
|
const files = Array.from(e.clipboardData.files);
|
|
615
766
|
if (files.length) {
|
|
@@ -618,6 +769,34 @@ export function ChatInput({
|
|
|
618
769
|
}
|
|
619
770
|
}}
|
|
620
771
|
onKeyDown={(e) => {
|
|
772
|
+
// 输入法组字时的按键归输入法(Esc 取消组字、↑↓ 选字、Enter 上屏),一律不当候选操作
|
|
773
|
+
if (mentionMode && !e.nativeEvent.isComposing) {
|
|
774
|
+
// 候选开着时优先于发送:Esc 关掉、↑↓ 选、Enter / Tab 采纳。
|
|
775
|
+
// 没有候选时不拦 Enter——`@某人` 这种不成候选的文字要能直接发出去。
|
|
776
|
+
if (e.key === "Escape") {
|
|
777
|
+
e.preventDefault();
|
|
778
|
+
closeMention(true);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (suggestions.length > 0) {
|
|
782
|
+
if (e.key === "ArrowDown") {
|
|
783
|
+
e.preventDefault();
|
|
784
|
+
setMentionIdx((i) => Math.min(i + 1, suggestions.length - 1));
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
if (e.key === "ArrowUp") {
|
|
788
|
+
e.preventDefault();
|
|
789
|
+
setMentionIdx((i) => Math.max(i - 1, 0));
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (e.key === "Enter" || e.key === "Tab") {
|
|
793
|
+
e.preventDefault();
|
|
794
|
+
const picked = suggestions[effectiveMentionIdx];
|
|
795
|
+
if (picked) applyMention(picked.candidate);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
621
800
|
if (adopted && text === "" && (e.key === "Backspace" || e.key === "Escape")) {
|
|
622
801
|
// 空参数时退格/Esc:摘掉命令胶囊
|
|
623
802
|
e.preventDefault();
|
|
@@ -710,6 +889,7 @@ export function ChatInput({
|
|
|
710
889
|
percent={contextPercent}
|
|
711
890
|
tokens={contextTokens}
|
|
712
891
|
contextWindow={contextWindow}
|
|
892
|
+
model={model}
|
|
713
893
|
source={contextSource}
|
|
714
894
|
/>
|
|
715
895
|
)}
|
|
@@ -772,17 +952,19 @@ export function ChatInput({
|
|
|
772
952
|
}
|
|
773
953
|
|
|
774
954
|
function fmtUploadSize(bytes: number): string {
|
|
775
|
-
|
|
776
|
-
if (bytes >= 1024) return `${Math.round(bytes / 1024)}KB`;
|
|
777
|
-
return `${bytes}B`;
|
|
955
|
+
return fmtBytes(bytes).replace(/ /g, "");
|
|
778
956
|
}
|
|
779
957
|
|
|
780
958
|
/**
|
|
781
|
-
* 引用 chip
|
|
782
|
-
*
|
|
959
|
+
* 引用 chip 上显示的短标签。只取 basename 会分不清两件事:目录与同名文件
|
|
960
|
+
* (`src/lib` 与 `src/lib.ts` 都显示 `lib`;`a/index.ts` 与 `b/index.ts` 都显示 `index.ts`)。
|
|
961
|
+
* 规则:目录带尾斜杠(`lib/`,配文件夹图标);其余最多保留**尾部两段**
|
|
962
|
+
* (`src/lib/index.ts` → `…/lib/index.ts`)。完整路径仍在 title 里。
|
|
783
963
|
*/
|
|
784
|
-
function
|
|
785
|
-
const
|
|
786
|
-
const
|
|
787
|
-
|
|
964
|
+
function referenceChipOf(path: string): { text: string; dir: boolean } {
|
|
965
|
+
const dir = /[\\/]$/.test(path);
|
|
966
|
+
const parts = path.replace(/[\\/]+$/, "").split(/[\\/]/).filter(Boolean);
|
|
967
|
+
if (!parts.length) return { text: path, dir };
|
|
968
|
+
const tail = parts.slice(-2).join("/");
|
|
969
|
+
return { text: dir ? `${tail}/` : parts.length > 2 ? `…/${tail}` : tail, dir };
|
|
788
970
|
}
|