@rexvane/piweb 0.3.14 → 0.3.16
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 +12 -9
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +6 -6
- package/.next/react-loadable-manifest.json +16 -4
- package/.next/required-server-files.js +3 -1
- package/.next/required-server-files.json +3 -1
- package/.next/routes-manifest.json +20 -0
- package/.next/server/app/_global-error/page.js +2 -2
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +7 -4
- package/.next/server/app/_not-found.segments/_full.segment.rsc +7 -4
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +5 -2
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +5 -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/config/route.js +1 -1
- package/.next/server/app/api/config/route.js.nft.json +1 -1
- package/.next/server/app/api/files/route.js +1 -1
- package/.next/server/app/api/files/route.js.nft.json +1 -1
- package/.next/server/app/api/git/route.js +2 -2
- package/.next/server/app/api/git/route.js.nft.json +1 -1
- package/.next/server/app/api/growth/route.js +1 -1
- package/.next/server/app/api/growth/route.js.nft.json +1 -1
- package/.next/server/app/api/health/route.js +1 -1
- package/.next/server/app/api/models/route.js +1 -1
- package/.next/server/app/api/models/route.js.nft.json +1 -1
- package/.next/server/app/api/pi-settings/route.js +1 -1
- package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
- package/.next/server/app/api/plugins/route.js +1 -1
- package/.next/server/app/api/plugins/route.js.nft.json +1 -1
- package/.next/server/app/api/prompts/route.js +1 -1
- package/.next/server/app/api/prompts/route.js.nft.json +1 -1
- package/.next/server/app/api/security/route.js +1 -1
- package/.next/server/app/api/security/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/import/route.js +3 -3
- package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/route.js +2 -2
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/swarm/[id]/route.js +1 -0
- package/.next/server/app/api/swarm/[id]/route.js.nft.json +1 -0
- package/.next/server/app/api/swarm/[id]/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/swarm/route.js +1 -0
- package/.next/server/app/api/swarm/route.js.nft.json +1 -0
- package/.next/server/app/api/swarm/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/tests/route.js +2 -0
- package/.next/server/app/api/tests/route.js.nft.json +1 -0
- package/.next/server/app/api/tests/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/update/route.js +2 -2
- package/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/server/app/api/version/route.js +1 -1
- package/.next/server/app/api/version/route.js.nft.json +1 -1
- package/.next/server/app/api/web-auth/route.js +1 -1
- package/.next/server/app/api/workspaces/route.js +1 -1
- package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
- package/.next/server/app/icon.svg/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +9 -6
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +7 -4
- package/.next/server/app/index.segments/_full.segment.rsc +9 -6
- package/.next/server/app/index.segments/_tree.segment.rsc +5 -2
- package/.next/server/app/login/page.js +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +1 -1
- package/.next/server/app/login.rsc +8 -5
- package/.next/server/app/login.segments/_full.segment.rsc +8 -5
- package/.next/server/app/login.segments/_tree.segment.rsc +5 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +6 -3
- package/.next/server/app/page.js +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +12 -9
- package/.next/server/chunks/496.js +1 -1
- package/.next/server/chunks/532.js +1 -1
- package/.next/server/chunks/686.js +6 -2
- package/.next/server/chunks/757.js +1 -0
- package/.next/server/chunks/775.js +3 -0
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/0t1jBUAuVaTFTXxRyIo3q/_buildManifest.js +1 -0
- package/.next/static/chunks/2608.43c58236c9cce8e6.js +1 -0
- package/.next/static/chunks/{2716.a9e808190a4e2424.js → 2716.4ec42df1d51354c9.js} +1 -1
- package/.next/static/chunks/4632.62139b565b657aac.js +1 -0
- package/.next/static/chunks/{4673.d3d0a7f2599080fb.js → 4673.7796104395deb8d7.js} +1 -1
- package/.next/static/chunks/4686-17f21a006739a596.js +1 -0
- package/.next/static/chunks/7577.10af9bb661652da8.js +1 -0
- package/.next/static/chunks/8866.38ec4ac4ee5a1b74.js +1 -0
- package/.next/static/chunks/app/_global-error/page-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/events/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/tree/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/agent/new/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/config/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/files/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/git/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/growth/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/health/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/models/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/pi-settings/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/plugins/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/prompts/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/security/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/export/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/sessions/import/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/sessions/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/skills/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/swarm/[id]/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/swarm/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/tests/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/update/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/version/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/web-auth/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/api/workspaces/route-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/app/page-14379b8060d5cf07.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-5148173b3f55e415.js +1 -0
- package/.next/static/chunks/webpack-d1a69ad9edb38b11.js +1 -0
- package/.next/static/css/d0a0162cb481abe8.css +1 -0
- package/.next/types/app/api/swarm/[id]/route.ts +353 -0
- package/.next/types/app/api/swarm/route.ts +353 -0
- package/.next/types/app/api/tests/route.ts +353 -0
- package/.next/types/routes.d.ts +4 -1
- package/.next/types/validator.ts +27 -0
- package/README.md +61 -7
- package/assets/archive.png +0 -0
- package/assets/growth-1.png +0 -0
- package/assets/growth-2.png +0 -0
- package/assets/import.png +0 -0
- package/assets/main.png +0 -0
- package/assets/outline.png +0 -0
- package/assets/prompts-1.png +0 -0
- package/assets/prompts-2.png +0 -0
- package/assets/providers-1.png +0 -0
- package/assets/providers-2.png +0 -0
- package/assets/providers-3.png +0 -0
- package/assets/trace-1.png +0 -0
- package/assets/trace-2.png +0 -0
- package/assets/trace-3.png +0 -0
- package/assets/trace-4.png +0 -0
- package/assets/viewer-1.png +0 -0
- package/assets/viewer-2.png +0 -0
- package/next.config.ts +6 -2
- package/package.json +10 -5
- package/scripts/launcher.mjs +1 -1
- package/scripts/managed-niubash-assets.mjs +146 -0
- package/scripts/pick-folder.ps1 +11 -2
- package/scripts/prepare-niubash-packages.mjs +182 -0
- package/scripts/release.mjs +11 -31
- package/scripts/run-test-file.mjs +34 -0
- package/scripts/smoke-managed-niubash.mjs +62 -0
- package/scripts/verify-package.mjs +53 -0
- package/scripts/verify-release-config.mjs +24 -0
- package/src/app/api/config/route.ts +4 -1
- package/src/app/api/security/route.ts +8 -3
- package/src/app/api/swarm/[id]/route.ts +28 -0
- package/src/app/api/swarm/route.ts +24 -0
- package/src/app/api/tests/route.ts +27 -0
- package/src/app/api/update/route.ts +8 -2
- package/src/app/globals.css +777 -0
- package/src/app/layout.tsx +13 -6
- package/src/components/AppShell.tsx +505 -199
- package/src/components/ChatInput.tsx +153 -26
- package/src/components/ChatWindow.tsx +46 -40
- package/src/components/ContextMeter.tsx +11 -17
- package/src/components/FileViewer.tsx +1 -1
- package/src/components/GitPanel.tsx +13 -11
- package/src/components/ModelSelector.tsx +19 -6
- package/src/components/PiMark.tsx +5 -38
- package/src/components/ProjectPanel.tsx +1 -0
- package/src/components/PromptPanel.tsx +132 -51
- package/src/components/SessionImportSection.tsx +53 -37
- package/src/components/SessionSidebar.tsx +159 -57
- package/src/components/SettingsPanel.tsx +330 -193
- package/src/components/SwarmCoordinatorPod.tsx +1021 -0
- package/src/components/TestInspectorPod.tsx +549 -0
- package/src/components/icons.tsx +23 -0
- package/src/hooks/usePiWeb.ts +112 -8
- package/src/i18n.tsx +59 -3
- package/src/lib/agent-manager.ts +182 -11
- package/src/lib/command-validation.ts +9 -1
- package/src/lib/delegation.ts +93 -0
- package/src/lib/managed-niubash.ts +231 -0
- package/src/lib/pi.ts +3 -2
- package/src/lib/runtime-activity.ts +27 -0
- package/src/lib/session-import/claude.ts +13 -2
- package/src/lib/session-import/codex.ts +44 -8
- package/src/lib/session-import/files.ts +14 -0
- package/src/lib/session-import/grok.ts +54 -24
- package/src/lib/session-import/index.ts +5 -0
- package/src/lib/session-import/lock.ts +25 -0
- package/src/lib/session-import/types.ts +12 -0
- package/src/lib/session-import/writer.ts +8 -3
- package/src/lib/skills-service.ts +16 -9
- package/src/lib/swarm-service.ts +371 -0
- package/src/lib/test-run-service.ts +122 -0
- package/src/lib/theme.ts +68 -1
- package/src/lib/trajectory.ts +1 -0
- package/src/lib/types.ts +14 -0
- package/src/lib/workflow.ts +55 -0
- package/.next/static/LOFl9bqqMV2goBV8erESO/_buildManifest.js +0 -1
- package/.next/static/chunks/2608.ec8ff1f5767c9056.js +0 -1
- package/.next/static/chunks/4632.112d2e893d48d188.js +0 -1
- package/.next/static/chunks/4686-7d694def920ff093.js +0 -1
- package/.next/static/chunks/app/_global-error/page-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/events/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/tree/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/agent/new/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/config/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/files/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/git/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/growth/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/health/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/models/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/pi-settings/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/plugins/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/prompts/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/security/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/export/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/sessions/import/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/skills/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/update/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/version/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/web-auth/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/api/workspaces/route-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/app/page-9a5440047c5a415c.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f77e9ede9808b18e.js +0 -1
- package/.next/static/chunks/webpack-d28e132e1b8c100c.js +0 -1
- package/.next/static/css/b2d35abc828d4713.css +0 -1
- package/assets/showcase.png +0 -0
- package/bin/pi.js +0 -50
- /package/.next/static/{LOFl9bqqMV2goBV8erESO → 0t1jBUAuVaTFTXxRyIo3q}/_ssgManifest.js +0 -0
|
@@ -8,16 +8,22 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { useEffect, useRef, useState } from "react";
|
|
10
10
|
import {
|
|
11
|
+
IconCheckOutline14,
|
|
12
|
+
IconChevronRight14,
|
|
13
|
+
IconCloseOutline14,
|
|
11
14
|
IconFileOutline16,
|
|
12
15
|
IconSendArrowUp14,
|
|
13
16
|
IconStopFill16,
|
|
14
17
|
IconTerminalOutline14,
|
|
18
|
+
IconWorkflowAgent16,
|
|
19
|
+
IconWorkflowGoal16,
|
|
20
|
+
IconWorkflowPlan16,
|
|
15
21
|
} from "@/components/icons";
|
|
16
22
|
import { ContextMeter } from "@/components/ContextMeter";
|
|
17
23
|
import { ModelSelector, type ModelChoice } from "@/components/ModelSelector";
|
|
18
24
|
import { useI18n } from "@/i18n";
|
|
19
25
|
import { otherBehavior, useEnterBehavior } from "@/lib/enter-behavior";
|
|
20
|
-
import type { ImageAttachment } from "@/lib/types";
|
|
26
|
+
import type { ImageAttachment, WorkflowMode, WorkflowState } from "@/lib/types";
|
|
21
27
|
|
|
22
28
|
export interface SlashCommand {
|
|
23
29
|
name: string;
|
|
@@ -56,6 +62,9 @@ export function ChatInput({
|
|
|
56
62
|
thinkingLevel,
|
|
57
63
|
thinkingLevels,
|
|
58
64
|
models,
|
|
65
|
+
modelLoading,
|
|
66
|
+
modelLoadError,
|
|
67
|
+
onRetryModels,
|
|
59
68
|
providerNames,
|
|
60
69
|
authByProvider,
|
|
61
70
|
queue,
|
|
@@ -68,6 +77,9 @@ export function ChatInput({
|
|
|
68
77
|
onSelectModel,
|
|
69
78
|
onSelectLevel,
|
|
70
79
|
onClearQueue,
|
|
80
|
+
workflow,
|
|
81
|
+
onWorkflowModeChange,
|
|
82
|
+
onApprovePlan,
|
|
71
83
|
draft,
|
|
72
84
|
initialDraft,
|
|
73
85
|
onDraftChange,
|
|
@@ -95,6 +107,9 @@ export function ChatInput({
|
|
|
95
107
|
thinkingLevel?: string;
|
|
96
108
|
thinkingLevels: string[];
|
|
97
109
|
models: ModelChoice[];
|
|
110
|
+
modelLoading?: boolean;
|
|
111
|
+
modelLoadError?: string | null;
|
|
112
|
+
onRetryModels?: () => void;
|
|
98
113
|
providerNames: Record<string, string>;
|
|
99
114
|
authByProvider: Record<string, boolean>;
|
|
100
115
|
queue: { steering: string[]; followUp: string[] };
|
|
@@ -109,6 +124,9 @@ export function ChatInput({
|
|
|
109
124
|
onSelectLevel: (level: string) => void;
|
|
110
125
|
/** 清空队列并返回被清掉的文本(取回编辑 / 丢弃都走它) */
|
|
111
126
|
onClearQueue?: () => Promise<QueueResult | void> | void;
|
|
127
|
+
workflow?: WorkflowState;
|
|
128
|
+
onWorkflowModeChange?: (mode: WorkflowMode) => void | Promise<{ success?: boolean } | void>;
|
|
129
|
+
onApprovePlan?: () => void | Promise<{ success?: boolean; error?: string } | void>;
|
|
112
130
|
/** Parent-owned, session-scoped draft. Async work always updates this same target. */
|
|
113
131
|
draft?: ChatDraft;
|
|
114
132
|
initialDraft?: ChatDraft;
|
|
@@ -146,15 +164,70 @@ export function ChatInput({
|
|
|
146
164
|
const [dragActive, setDragActive] = useState(false);
|
|
147
165
|
const dragDepth = useRef(0);
|
|
148
166
|
const [modelOpen, setModelOpen] = useState(false);
|
|
167
|
+
const [workflowOpen, setWorkflowOpen] = useState(false);
|
|
168
|
+
const [pendingWorkflowMode, setPendingWorkflowMode] = useState<WorkflowMode | null>(null);
|
|
169
|
+
const [approvalPending, setApprovalPending] = useState(false);
|
|
170
|
+
const [approvalError, setApprovalError] = useState("");
|
|
171
|
+
const approvalInFlightRef = useRef(false);
|
|
172
|
+
const workflowRequestRef = useRef(0);
|
|
173
|
+
const workflowInFlightRef = useRef(false);
|
|
174
|
+
const queuedWorkflowRef = useRef<{ mode: WorkflowMode; request: number } | null>(null);
|
|
149
175
|
const [cmdIdx, setCmdIdx] = useState(0);
|
|
150
176
|
const [cmdDismissed, setCmdDismissed] = useState(false);
|
|
151
177
|
const taRef = useRef<HTMLTextAreaElement>(null);
|
|
152
178
|
const wrapRef = useRef<HTMLDivElement>(null);
|
|
179
|
+
const workflowRef = useRef<HTMLDivElement>(null);
|
|
153
180
|
const { t } = useI18n();
|
|
154
181
|
const isBlocked = !model?.id;
|
|
155
182
|
const enterBehavior = useEnterBehavior();
|
|
156
183
|
const queuedCount = queue.steering.length + queue.followUp.length;
|
|
157
184
|
const [restoreTick, setRestoreTick] = useState(0);
|
|
185
|
+
const activeWorkflowMode = pendingWorkflowMode ?? workflow?.mode ?? "agent";
|
|
186
|
+
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (pendingWorkflowMode !== null && workflow?.mode === pendingWorkflowMode) setPendingWorkflowMode(null);
|
|
189
|
+
}, [workflow?.mode, pendingWorkflowMode]);
|
|
190
|
+
useEffect(() => { setApprovalError(""); }, [workflow?.mode, workflow?.planStatus, workflow?.planId]);
|
|
191
|
+
|
|
192
|
+
const approvePlan = async () => {
|
|
193
|
+
if (!onApprovePlan || approvalInFlightRef.current || isStreaming || disabled) return;
|
|
194
|
+
approvalInFlightRef.current = true;
|
|
195
|
+
setApprovalPending(true);
|
|
196
|
+
setApprovalError("");
|
|
197
|
+
try {
|
|
198
|
+
const result = await onApprovePlan();
|
|
199
|
+
if (result && result.success === false && mountedRef.current) setApprovalError(result.error || t.planApprovalFailed);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (mountedRef.current) setApprovalError(error instanceof Error ? error.message : t.planApprovalFailed);
|
|
202
|
+
} finally {
|
|
203
|
+
approvalInFlightRef.current = false;
|
|
204
|
+
if (mountedRef.current) setApprovalPending(false);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const flushWorkflowMode = async () => {
|
|
209
|
+
if (workflowInFlightRef.current || !queuedWorkflowRef.current || !onWorkflowModeChange) return;
|
|
210
|
+
const { mode, request } = queuedWorkflowRef.current;
|
|
211
|
+
queuedWorkflowRef.current = null;
|
|
212
|
+
workflowInFlightRef.current = true;
|
|
213
|
+
try {
|
|
214
|
+
const result = await onWorkflowModeChange(mode);
|
|
215
|
+
if (mountedRef.current && request === workflowRequestRef.current && result && result.success === false) setPendingWorkflowMode(null);
|
|
216
|
+
} catch {
|
|
217
|
+
if (mountedRef.current && request === workflowRequestRef.current) setPendingWorkflowMode(null);
|
|
218
|
+
} finally {
|
|
219
|
+
workflowInFlightRef.current = false;
|
|
220
|
+
if (mountedRef.current && queuedWorkflowRef.current) void flushWorkflowMode();
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const selectWorkflowMode = (mode: WorkflowMode) => {
|
|
225
|
+
if (!onWorkflowModeChange) return;
|
|
226
|
+
const request = ++workflowRequestRef.current;
|
|
227
|
+
setPendingWorkflowMode(mode);
|
|
228
|
+
queuedWorkflowRef.current = { mode, request };
|
|
229
|
+
void flushWorkflowMode();
|
|
230
|
+
};
|
|
158
231
|
|
|
159
232
|
useEffect(() => {
|
|
160
233
|
mountedRef.current = true;
|
|
@@ -163,6 +236,23 @@ export function ChatInput({
|
|
|
163
236
|
useEffect(() => {
|
|
164
237
|
setAttachmentError(uploadFailure ?? "");
|
|
165
238
|
}, [uploadFailure]);
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
if (!workflowOpen) return;
|
|
241
|
+
const closeOnPointer = (event: PointerEvent) => {
|
|
242
|
+
if (!workflowRef.current?.contains(event.target as Node)) setWorkflowOpen(false);
|
|
243
|
+
};
|
|
244
|
+
const closeOnEscape = (event: KeyboardEvent) => {
|
|
245
|
+
if (event.key !== "Escape") return;
|
|
246
|
+
setWorkflowOpen(false);
|
|
247
|
+
workflowRef.current?.querySelector<HTMLButtonElement>(".pw-workflow-add")?.focus();
|
|
248
|
+
};
|
|
249
|
+
document.addEventListener("pointerdown", closeOnPointer);
|
|
250
|
+
document.addEventListener("keydown", closeOnEscape);
|
|
251
|
+
return () => {
|
|
252
|
+
document.removeEventListener("pointerdown", closeOnPointer);
|
|
253
|
+
document.removeEventListener("keydown", closeOnEscape);
|
|
254
|
+
};
|
|
255
|
+
}, [workflowOpen]);
|
|
166
256
|
useEffect(() => {
|
|
167
257
|
autoSize();
|
|
168
258
|
}, [text]);
|
|
@@ -404,9 +494,35 @@ export function ChatInput({
|
|
|
404
494
|
}, []);
|
|
405
495
|
|
|
406
496
|
const hasDraft = text.trim().length > 0 || images.length > 0 || uploads.length > 0;
|
|
497
|
+
const workflowName = activeWorkflowMode === "plan" ? t.workflowPlan : activeWorkflowMode === "goal" ? t.workflowGoal : t.workflowAgent;
|
|
498
|
+
const workflowPlaceholder = activeWorkflowMode === "plan" ? t.workflowPlanPlaceholder : activeWorkflowMode === "goal" ? t.workflowGoalPlaceholder : t.inputPlaceholder;
|
|
499
|
+
const composerExpanded = text.length > 56 || text.includes("\n") || images.length > 0 || uploads.length > 0 || Boolean(adopted);
|
|
407
500
|
|
|
408
501
|
return (
|
|
409
|
-
<div ref={wrapRef} className="relative w-full" data-testid="composer">
|
|
502
|
+
<div ref={wrapRef} className="pw-composer-shell relative w-full" data-expanded={composerExpanded || undefined} data-testid="composer">
|
|
503
|
+
{activeWorkflowMode === "plan" && workflow?.mode === "plan" && workflow.planStatus === "ready" && onApprovePlan && (
|
|
504
|
+
<section className="pw-plan-ready" aria-label={t.planReady}>
|
|
505
|
+
<div className="pw-plan-ready-content">
|
|
506
|
+
<span className="pw-plan-ready-icon" aria-hidden="true"><IconWorkflowPlan16 size={16} /></span>
|
|
507
|
+
<div className="pw-plan-ready-copy">
|
|
508
|
+
<strong>{t.planReady}</strong>
|
|
509
|
+
<span>{t.planReadyHint}</span>
|
|
510
|
+
</div>
|
|
511
|
+
</div>
|
|
512
|
+
<div className="pw-plan-ready-actions">
|
|
513
|
+
<button type="button" className="pw-plan-ready-refine" disabled={disabled || approvalPending} onClick={() => taRef.current?.focus()}>{t.planRefine}</button>
|
|
514
|
+
<button type="button" className="pw-plan-ready-approve" disabled={isStreaming || disabled || approvalPending} onClick={() => { void approvePlan(); }}>{approvalPending ? t.planApproving : t.planApprove}<IconChevronRight14 size={14} aria-hidden="true" /></button>
|
|
515
|
+
</div>
|
|
516
|
+
{approvalError && <p className="pw-plan-ready-error" role="alert">{approvalError}</p>}
|
|
517
|
+
</section>
|
|
518
|
+
)}
|
|
519
|
+
{activeWorkflowMode === "goal" && workflow?.mode === "goal" && workflow.goal && (
|
|
520
|
+
<div className="pw-goal-active" title={workflow.goal}>
|
|
521
|
+
<IconWorkflowGoal16 size={14} aria-hidden="true" />
|
|
522
|
+
<span className="pw-goal-active-label">{t.goalActive}</span>
|
|
523
|
+
<span className="pw-goal-active-text">{workflow.goal}</span>
|
|
524
|
+
</div>
|
|
525
|
+
)}
|
|
410
526
|
{/* 排队中的消息(Claude Code 同款:列在输入卡上方;↑ 取回编辑,或直接丢弃) */}
|
|
411
527
|
{queuedCount > 0 && (
|
|
412
528
|
<div className="pw-queue">
|
|
@@ -518,16 +634,8 @@ export function ChatInput({
|
|
|
518
634
|
)}
|
|
519
635
|
|
|
520
636
|
{/* 卡片(玻璃态)—— 拖放由 document 级监听统一接管 */}
|
|
521
|
-
<div
|
|
522
|
-
className="
|
|
523
|
-
style={{
|
|
524
|
-
borderRadius: 22,
|
|
525
|
-
background: "var(--dsw-input-major)",
|
|
526
|
-
boxShadow: "var(--dsw-elevation-soft)",
|
|
527
|
-
border: "0.5px solid var(--dsw-border-l1)",
|
|
528
|
-
}}
|
|
529
|
-
>
|
|
530
|
-
<div className="px-4 pt-3 flex items-start gap-1.5">
|
|
637
|
+
<div className="pw-composer relative w-full" data-mode={activeWorkflowMode}>
|
|
638
|
+
<div className="pw-composer-body flex items-start gap-1.5">
|
|
531
639
|
{adopted && (
|
|
532
640
|
<button
|
|
533
641
|
type="button"
|
|
@@ -555,7 +663,7 @@ export function ChatInput({
|
|
|
555
663
|
value={text}
|
|
556
664
|
disabled={disabled || isBlocked || sendBusy}
|
|
557
665
|
rows={1}
|
|
558
|
-
placeholder={adopted ? (adopted.argumentHint ??
|
|
666
|
+
placeholder={adopted ? (adopted.argumentHint ?? workflowPlaceholder) : !disabled && isBlocked ? t.blockedComposer : workflowPlaceholder}
|
|
559
667
|
suppressHydrationWarning
|
|
560
668
|
className="min-w-0 flex-1 resize-none"
|
|
561
669
|
style={{ fontSize: "var(--piweb-chat-font-size, var(--dsh-content-font-size))", lineHeight: 1.55 }}
|
|
@@ -631,19 +739,35 @@ export function ChatInput({
|
|
|
631
739
|
</div>
|
|
632
740
|
|
|
633
741
|
{/* 按钮行:左侧只保留命令入口;图片通过拖放或粘贴添加。 */}
|
|
634
|
-
<div
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
742
|
+
<div className="pw-composer-toolbar flex items-center gap-2">
|
|
743
|
+
{workflow && onWorkflowModeChange && (
|
|
744
|
+
<div ref={workflowRef} className="relative flex shrink-0 items-center gap-2">
|
|
745
|
+
<button type="button" className="pw-workflow-add" disabled={isStreaming || disabled} aria-label={`${t.workflowMenu}: ${workflowName}`} title={t.workflowMenu} aria-haspopup="menu" aria-expanded={workflowOpen} onClick={() => setWorkflowOpen((open) => !open)}>
|
|
746
|
+
<span className="pw-workflow-plus" aria-hidden="true" />
|
|
747
|
+
</button>
|
|
748
|
+
{activeWorkflowMode !== "agent" && (
|
|
749
|
+
<span className="pw-workflow-current" data-mode={activeWorkflowMode}>
|
|
750
|
+
<span className="pw-workflow-current-icon" aria-hidden="true">{activeWorkflowMode === "plan" ? <IconWorkflowPlan16 size={14} /> : <IconWorkflowGoal16 size={14} />}</span>
|
|
751
|
+
<span>{workflowName}</span>
|
|
752
|
+
<button type="button" className="pw-workflow-current-dismiss" disabled={isStreaming || disabled} aria-label={`${t.workflowClear}: ${workflowName}`} title={t.workflowClear} onClick={() => { selectWorkflowMode("agent"); taRef.current?.focus(); }}><IconCloseOutline14 size={10} aria-hidden="true" /></button>
|
|
753
|
+
</span>
|
|
754
|
+
)}
|
|
755
|
+
{workflowOpen && (
|
|
756
|
+
<div className="popover pw-workflow-menu" role="menu">
|
|
757
|
+
<div className="pw-workflow-menu-label">{t.workflowMenu}</div>
|
|
758
|
+
{(["agent", "plan", "goal"] as const).map((mode) => (
|
|
759
|
+
<button key={mode} type="button" className="pw-workflow-option" data-mode={mode} role="menuitemradio" aria-checked={activeWorkflowMode === mode} onClick={() => { setWorkflowOpen(false); selectWorkflowMode(mode); taRef.current?.focus(); }}>
|
|
760
|
+
<span className="pw-workflow-option-icon" aria-hidden="true">{mode === "agent" ? <IconWorkflowAgent16 size={16} /> : mode === "plan" ? <IconWorkflowPlan16 size={16} /> : <IconWorkflowGoal16 size={16} />}</span>
|
|
761
|
+
<strong className="pw-workflow-option-name">{mode === "agent" ? t.workflowAgent : mode === "plan" ? t.workflowPlan : t.workflowGoal}</strong>
|
|
762
|
+
<span className="pw-workflow-option-description">{mode === "agent" ? t.workflowAgentHint : mode === "plan" ? t.workflowPlanHint : t.workflowGoalHint}</span>
|
|
763
|
+
<IconCheckOutline14 className="pw-workflow-option-check" size={14} aria-hidden="true" />
|
|
764
|
+
</button>
|
|
765
|
+
))}
|
|
766
|
+
</div>
|
|
767
|
+
)}
|
|
768
|
+
</div>
|
|
769
|
+
)}
|
|
770
|
+
{!workflow && <div className="flex-1" />}
|
|
647
771
|
|
|
648
772
|
{/* 模型芯片(右组,dsh 分组菜单;/model 命令可受控打开) */}
|
|
649
773
|
<ModelSelector
|
|
@@ -651,6 +775,9 @@ export function ChatInput({
|
|
|
651
775
|
thinkingLevel={thinkingLevel}
|
|
652
776
|
thinkingLevels={thinkingLevels}
|
|
653
777
|
models={models}
|
|
778
|
+
modelLoading={modelLoading}
|
|
779
|
+
modelLoadError={modelLoadError}
|
|
780
|
+
onRetryModels={onRetryModels}
|
|
654
781
|
providerNames={providerNames}
|
|
655
782
|
authByProvider={authByProvider}
|
|
656
783
|
onSelectModel={onSelectModel}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
IconFileOutline16,
|
|
23
23
|
IconRefreshOutline14,
|
|
24
24
|
IconSearchOutline16,
|
|
25
|
+
IconSendArrowUp14,
|
|
25
26
|
IconTerminalOutline14,
|
|
26
27
|
IconThinkOutline14,
|
|
27
28
|
IconWarningOutline16,
|
|
@@ -78,7 +79,7 @@ function formatMessageClock(time: number, t: Dict): string {
|
|
|
78
79
|
|
|
79
80
|
const Markdown = memo(function Markdown({ text }: { text: string }) {
|
|
80
81
|
return (
|
|
81
|
-
<div className="md">
|
|
82
|
+
<div className="md select-text">
|
|
82
83
|
<ReactMarkdown
|
|
83
84
|
remarkPlugins={[remarkGfm]}
|
|
84
85
|
rehypePlugins={[[rehypeHighlight, { detect: false, ignoreMissing: true }]]}
|
|
@@ -897,72 +898,77 @@ const UserMessage = memo(function UserMessage({ message, onEditMessage }: { mess
|
|
|
897
898
|
const images = message.content.filter((content) => content.type === "image");
|
|
898
899
|
const [editing, setEditing] = useState(false);
|
|
899
900
|
const [draft, setDraft] = useState(text);
|
|
901
|
+
const [editorWidth, setEditorWidth] = useState(132);
|
|
902
|
+
const editRef = useRef<HTMLTextAreaElement>(null);
|
|
903
|
+
const bubbleRef = useRef<HTMLDivElement>(null);
|
|
900
904
|
const canEdit = Boolean(onEditMessage && message.id && text);
|
|
905
|
+
useEffect(() => {
|
|
906
|
+
if (!editing || !editRef.current) return;
|
|
907
|
+
editRef.current.style.height = "0px";
|
|
908
|
+
editRef.current.style.height = `${Math.min(320, editRef.current.scrollHeight)}px`;
|
|
909
|
+
}, [draft, editing]);
|
|
910
|
+
const imageStrip = images.length > 0 && (
|
|
911
|
+
<div className="mb-2 flex max-w-[85%] flex-wrap justify-end gap-2">
|
|
912
|
+
{images.map((image, index) => (
|
|
913
|
+
<img key={index} src={`data:${image.mimeType};base64,${image.data}`} alt="" className="max-h-72 max-w-full rounded-2xl object-contain" style={{ border: "0.5px solid var(--dsw-border-l2)" }} />
|
|
914
|
+
))}
|
|
915
|
+
</div>
|
|
916
|
+
);
|
|
917
|
+
function beginEdit() {
|
|
918
|
+
if (!canEdit) return;
|
|
919
|
+
setEditorWidth(Math.max(132, Math.ceil(bubbleRef.current?.getBoundingClientRect().width ?? 132)));
|
|
920
|
+
setDraft(text);
|
|
921
|
+
setEditing(true);
|
|
922
|
+
}
|
|
901
923
|
if (editing) {
|
|
902
924
|
return (
|
|
903
925
|
<div className="group mt-7 flex w-full flex-col items-end first:mt-0" data-role="user">
|
|
904
|
-
|
|
926
|
+
{imageStrip}
|
|
927
|
+
<div className="msg-user-bubble pw-user-editor" style={{ width: editorWidth }}>
|
|
905
928
|
<textarea
|
|
906
|
-
|
|
907
|
-
|
|
929
|
+
ref={editRef}
|
|
930
|
+
className="pw-user-editor-input"
|
|
908
931
|
value={draft}
|
|
909
932
|
aria-label={t.editMessage}
|
|
910
933
|
autoFocus
|
|
911
934
|
onChange={(e) => setDraft(e.target.value)}
|
|
912
935
|
onKeyDown={(e) => {
|
|
913
936
|
if (e.key === "Escape") { e.preventDefault(); setEditing(false); }
|
|
914
|
-
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); submit(); }
|
|
937
|
+
if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); submit(); }
|
|
915
938
|
}}
|
|
916
939
|
/>
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
</button>
|
|
922
|
-
<button
|
|
923
|
-
type="button"
|
|
924
|
-
className="btn-primary-white"
|
|
925
|
-
style={{ height: 26, padding: "0 12px", fontSize: 12 }}
|
|
926
|
-
disabled={!draft.trim() || draft.trim() === text.trim()}
|
|
927
|
-
onClick={submit}
|
|
928
|
-
>
|
|
929
|
-
{t.sendEdit}
|
|
930
|
-
</button>
|
|
940
|
+
<div className="pw-user-editor-actions">
|
|
941
|
+
<button type="button" className="pw-user-editor-cancel" aria-label={t.cancel} title={t.cancel} onClick={() => setEditing(false)}><IconCloseOutline14 size={14} /></button>
|
|
942
|
+
<button type="button" className="pw-user-editor-send" aria-label={t.sendEdit} title={t.sendEdit} disabled={!draft.trim()} onClick={submit}><IconSendArrowUp14 size={15} /></button>
|
|
943
|
+
</div>
|
|
931
944
|
</div>
|
|
932
945
|
</div>
|
|
933
946
|
);
|
|
934
947
|
}
|
|
935
948
|
function submit() {
|
|
936
949
|
const next = draft.trim();
|
|
937
|
-
if (!next ||
|
|
950
|
+
if (!next || !onEditMessage || !message.id) return;
|
|
938
951
|
setEditing(false);
|
|
939
952
|
onEditMessage(message.id, next);
|
|
940
953
|
}
|
|
941
954
|
return (
|
|
942
955
|
// 回合边界:用户消息前留 28px(比回合内 8px 大得多),长对话里一眼找到“这一轮从哪开始”
|
|
943
956
|
<div className="group mt-7 flex w-full flex-col items-end first:mt-0" data-role="user">
|
|
944
|
-
{
|
|
945
|
-
<div className="mb-2 flex max-w-[85%] flex-wrap justify-end gap-2">
|
|
946
|
-
{images.map((image, index) => (
|
|
947
|
-
<img
|
|
948
|
-
key={index}
|
|
949
|
-
src={`data:${image.mimeType};base64,${image.data}`}
|
|
950
|
-
alt=""
|
|
951
|
-
className="max-h-72 max-w-full rounded-2xl object-contain"
|
|
952
|
-
style={{ border: "0.5px solid var(--dsw-border-l2)" }}
|
|
953
|
-
/>
|
|
954
|
-
))}
|
|
955
|
-
</div>
|
|
956
|
-
)}
|
|
957
|
+
{imageStrip}
|
|
957
958
|
{/* 用户消息也走 Markdown:贴进来的代码块/列表不再是一坨纯文本 */}
|
|
958
|
-
{text && <div className=
|
|
959
|
+
{text && <div ref={bubbleRef} className={`msg-user-bubble clay-bubble-user${canEdit ? " pw-user-message-clickable" : ""}`} onClick={canEdit ? (event) => {
|
|
960
|
+
if ((event.target as HTMLElement).closest("a, button, input, textarea, [role='button']")) return;
|
|
961
|
+
const selection = window.getSelection();
|
|
962
|
+
if (selection && !selection.isCollapsed) return;
|
|
963
|
+
beginEdit();
|
|
964
|
+
} : undefined}><Markdown text={text} /></div>}
|
|
959
965
|
{/* 用户消息:复制 + 原地编辑重发(编辑后模型从这条消息重新回答);时钟在图标左侧(dsh clock=start) */}
|
|
960
966
|
{text && (
|
|
961
967
|
<MessageActions
|
|
962
968
|
text={text}
|
|
963
969
|
clockStart={message.timestamp !== undefined}
|
|
964
970
|
time={message.timestamp}
|
|
965
|
-
onEdit={canEdit ?
|
|
971
|
+
onEdit={canEdit ? beginEdit : undefined}
|
|
966
972
|
/>
|
|
967
973
|
)}
|
|
968
974
|
</div>
|
|
@@ -1084,7 +1090,7 @@ function TurnBlockView({
|
|
|
1084
1090
|
const showProcess = items.length > 0 || Boolean(contextFiles?.length) || turnLive || (isLast && Boolean(retryNotice));
|
|
1085
1091
|
|
|
1086
1092
|
return (
|
|
1087
|
-
|
|
1093
|
+
<div className="pw-conversation-turn">
|
|
1088
1094
|
{user && <UserMessage message={user} onEditMessage={onEditMessage} />}
|
|
1089
1095
|
{showProcess && (
|
|
1090
1096
|
<div className="pw-turn">
|
|
@@ -1115,7 +1121,7 @@ function TurnBlockView({
|
|
|
1115
1121
|
</div>
|
|
1116
1122
|
)}
|
|
1117
1123
|
{finalIdx >= 0 && <FinalAnswer message={messages[finalIdx]} onFork={onFork} showActions={!isStreaming || !isLast} turnStartMs={user?.timestamp} />}
|
|
1118
|
-
|
|
1124
|
+
</div>
|
|
1119
1125
|
);
|
|
1120
1126
|
}
|
|
1121
1127
|
|
|
@@ -1315,10 +1321,10 @@ export function ChatWindow({
|
|
|
1315
1321
|
};
|
|
1316
1322
|
|
|
1317
1323
|
return (
|
|
1318
|
-
<div className="relative flex h-full min-h-0 flex-col">
|
|
1324
|
+
<div className="pw-chat-window relative flex h-full min-h-0 flex-col">
|
|
1319
1325
|
<div
|
|
1320
1326
|
ref={scrollRef}
|
|
1321
|
-
className="min-h-0 flex-1 overflow-y-auto"
|
|
1327
|
+
className="pw-chat-scroll min-h-0 flex-1 overflow-y-auto"
|
|
1322
1328
|
role="log"
|
|
1323
1329
|
aria-live="polite"
|
|
1324
1330
|
aria-relevant="additions"
|
|
@@ -1330,7 +1336,7 @@ export function ChatWindow({
|
|
|
1330
1336
|
if (atBottom) setUnseen(0);
|
|
1331
1337
|
}}
|
|
1332
1338
|
>
|
|
1333
|
-
<div className="mx-auto flex w-full flex-col px-4 py-6" style={{ maxWidth: "var(--dsh-chat-content-width)" }}>
|
|
1339
|
+
<div className="pw-chat-flow mx-auto flex w-full flex-col px-4 py-6" style={{ maxWidth: "var(--dsh-chat-content-width)" }}>
|
|
1334
1340
|
{turns.map((turn, ti) => (
|
|
1335
1341
|
<TurnBlock
|
|
1336
1342
|
key={turn.key}
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* 上下文占用:
|
|
5
|
-
* 圆环常驻按钮 +
|
|
6
|
-
*
|
|
7
|
-
* 分段口径:全部为字符/4 启发式估算(与 SDK estimateTokens 一致),
|
|
5
|
+
* 圆环常驻按钮 + 弹窗显示 Pi 的上下文用量与内容构成估算。
|
|
6
|
+
* 总量由 SDK 提供;分类是字符/4 启发式估算,不能与总量相加。
|
|
8
7
|
* 服务端分项(System Prompt / Tools / Memory / Skills / Compacted / Buffer)来自快照,
|
|
9
8
|
* 消息分项(User / Agent Text / Thinking / Tool Call / Tool Output)弹窗打开时前端统计。
|
|
10
9
|
* 无对话时不渲染(由父组件经 contextVisible 控制)。
|
|
@@ -18,7 +17,7 @@ function fmtTok(n: number): string {
|
|
|
18
17
|
return n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(Math.round(n));
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
/**
|
|
20
|
+
/** 估算分类的定义(顺序即渲染顺序) */
|
|
22
21
|
interface Segment {
|
|
23
22
|
key: string;
|
|
24
23
|
label: string;
|
|
@@ -91,7 +90,6 @@ export function ContextMeter({
|
|
|
91
90
|
{ key: "agentToolCall", label: t.contextAgentToolCall, tokens: messageSegments.agentToolCall, color: "#D3A15F" },
|
|
92
91
|
{ key: "toolOutput", label: t.contextToolOutput, tokens: messageSegments.toolOutput, color: "var(--dsw-warn)" },
|
|
93
92
|
{ key: "compacted", label: t.contextCompacted, tokens: b.compacted, color: "#A05A78" },
|
|
94
|
-
{ key: "buffer", label: t.contextAutoCompactBuffer, tokens: b.autoCompactBuffer, color: "var(--dsw-border-l3)" },
|
|
95
93
|
]
|
|
96
94
|
: // 服务端分项缺失(旧快照):退回旧三段口径
|
|
97
95
|
[
|
|
@@ -100,11 +98,9 @@ export function ContextMeter({
|
|
|
100
98
|
{ key: "tools", label: t.contextTools, tokens: Math.max(0, used - Math.max(0, Math.round((source?.systemChars ?? 0) / 4)) - messageSegments.user - messageSegments.agentText - messageSegments.agentThinking), color: "var(--dsw-warn)" },
|
|
101
99
|
];
|
|
102
100
|
|
|
103
|
-
//
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
const barTotal = window_ > 0 ? window_ : usedSum || 1;
|
|
107
|
-
const bar = (n: number) => `${(n / barTotal) * 100}%`;
|
|
101
|
+
// 容量只能按 SDK 总量计算;分类有遗漏与重叠,不能用于推导剩余空间。
|
|
102
|
+
const free = window_ > 0 && tokens !== null ? Math.max(0, window_ - used) : null;
|
|
103
|
+
const usedWidth = window_ > 0 && tokens !== null ? `${Math.max(0, Math.min(100, (used / window_) * 100))}%` : "0%";
|
|
108
104
|
|
|
109
105
|
return (
|
|
110
106
|
<div ref={ref} className="relative">
|
|
@@ -139,21 +135,19 @@ export function ContextMeter({
|
|
|
139
135
|
{tokens === null ? "—" : `~${fmtTok(used)}`} / {window_ > 0 ? fmtTok(window_) : "—"}
|
|
140
136
|
</div>
|
|
141
137
|
|
|
142
|
-
{
|
|
143
|
-
{usedSum > 0 && (
|
|
138
|
+
{free !== null && (
|
|
144
139
|
<div className="mt-3 flex h-2 w-full overflow-hidden rounded-full" style={{ background: "var(--dsw-selector)" }} aria-hidden>
|
|
145
|
-
{
|
|
146
|
-
<span key={s.key} style={{ width: bar(s.tokens), background: s.color }} />
|
|
147
|
-
))}
|
|
148
|
-
{free > 0 && <span style={{ width: bar(free), background: "transparent" }} />}
|
|
140
|
+
<span style={{ width: usedWidth, background: "var(--dsw-accent)" }} />
|
|
149
141
|
</div>
|
|
150
142
|
)}
|
|
143
|
+
<div className="mt-3" style={{ color: "var(--dsw-label-caption)", fontSize: 11 }}>{t.contextEstimateNote}</div>
|
|
151
144
|
<div className="mt-2 flex flex-col gap-1" style={{ color: "var(--dsw-label-tertiary)" }}>
|
|
152
145
|
{segments.filter((s) => s.tokens > 0).map((s) => (
|
|
153
146
|
<SegmentRow key={s.key} label={s.label} tokens={s.tokens} color={s.color} />
|
|
154
147
|
))}
|
|
155
|
-
{window_ > 0 && <SegmentRow label={t.contextFreeSpace} tokens={free} color="var(--dsw-selector)" />}
|
|
156
148
|
</div>
|
|
149
|
+
{b && b.autoCompactBuffer > 0 && <div className="mt-2 flex justify-between gap-2" style={{ color: "var(--dsw-label-tertiary)" }}><span>{t.contextAutoCompactBuffer}</span><span>{fmtTok(b.autoCompactBuffer)}</span></div>}
|
|
150
|
+
{window_ > 0 && <div className="mt-2 flex justify-between gap-2" style={{ color: "var(--dsw-label-secondary)" }}><span>{t.contextFreeSpace}</span><span>{free === null ? "—" : `~${fmtTok(free)}`}</span></div>}
|
|
157
151
|
</div>
|
|
158
152
|
)}
|
|
159
153
|
</div>
|
|
@@ -463,7 +463,7 @@ export function FileViewer({
|
|
|
463
463
|
<div className="pw-viewer-toolbar hairline-b">
|
|
464
464
|
<div className="flex min-w-0 flex-1 items-center gap-1.5 rounded-lg px-2" style={{ background: "var(--dsw-hover)", height: 26, maxWidth: 360 }}>
|
|
465
465
|
<IconSearchOutline16 size={12} style={{ flex: "none", color: "var(--dsw-label-caption)" }} />
|
|
466
|
-
<input value={treeFilter} onChange={(e) => setTreeFilter(e.target.value)} placeholder={t.growthFilter} className="min-w-0 flex-1 bg-transparent" style={{ fontSize: 12 }} />
|
|
466
|
+
<input value={treeFilter} onChange={(e) => setTreeFilter(e.target.value)} aria-label={t.growthFilter} placeholder={t.growthFilter} className="min-w-0 flex-1 bg-transparent" style={{ fontSize: 12 }} />
|
|
467
467
|
</div>
|
|
468
468
|
<span style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
|
|
469
469
|
{roundNumber ? t.growthRoundLabel.replace("{n}", String(roundNumber)) : ""}
|
|
@@ -181,27 +181,29 @@ export function GitPanel({
|
|
|
181
181
|
: t.gitPanel;
|
|
182
182
|
|
|
183
183
|
return (
|
|
184
|
-
<div className="flex h-full min-h-0 flex-col p-4">
|
|
185
|
-
<div className="mb-3 flex items-center gap-2">
|
|
184
|
+
<div className="flex h-full min-h-0 flex-col p-4 select-none">
|
|
185
|
+
<div className="mb-3 flex items-center gap-2 pb-2 border-b border-slate-200/60 dark:border-slate-800/60">
|
|
186
186
|
{view ? (
|
|
187
|
-
<button className="
|
|
187
|
+
<button className="w-7 h-7 rounded-full clay-btn-soft flex items-center justify-center text-slate-600 hover:text-slate-900 transition-transform active:scale-95" title={t.gitBack} onClick={() => changeView(null)}>
|
|
188
188
|
<IconChevronLeft14 size={13} />
|
|
189
189
|
</button>
|
|
190
190
|
) : (
|
|
191
|
-
<
|
|
191
|
+
<div className="w-7 h-7 rounded-full clay-inset flex items-center justify-center text-emerald-600">
|
|
192
|
+
<IconGitOutline16 size={15} />
|
|
193
|
+
</div>
|
|
192
194
|
)}
|
|
193
|
-
<span className="min-w-0 flex-1 truncate
|
|
195
|
+
<span className="min-w-0 flex-1 truncate font-bold text-sm text-slate-800 dark:text-slate-100" title={view?.kind === "file" ? view.path : undefined}>
|
|
194
196
|
{headerTitle}
|
|
195
197
|
</span>
|
|
196
198
|
{view?.kind === "file" && onOpenFile && info?.root && (
|
|
197
|
-
<button className="
|
|
199
|
+
<button className="w-7 h-7 rounded-full clay-btn-soft flex items-center justify-center text-slate-600 hover:text-slate-900 transition-transform active:scale-95" title={t.fileOpenEditor} onClick={() => onOpenFile(`${info.root}/${view.path}`)}>
|
|
198
200
|
<IconFileOutline16 size={13} />
|
|
199
201
|
</button>
|
|
200
202
|
)}
|
|
201
|
-
<button className="
|
|
202
|
-
<IconRefreshOutline14 size={13} />
|
|
203
|
+
<button className="w-7 h-7 rounded-full clay-btn-soft flex items-center justify-center text-slate-600 hover:text-slate-900 transition-transform active:scale-95" title={t.panelRefresh} onClick={() => (view ? void loadDiff(view) : void load())}>
|
|
204
|
+
<IconRefreshOutline14 size={13} className={loading || diffLoading ? "piweb-spin" : undefined} />
|
|
203
205
|
</button>
|
|
204
|
-
<button className="
|
|
206
|
+
<button className="w-7 h-7 rounded-full clay-btn-soft flex items-center justify-center text-slate-600 hover:text-rose-600 transition-colors" title={t.close} onClick={onClose}>
|
|
205
207
|
<IconCloseOutline14 size={13} />
|
|
206
208
|
</button>
|
|
207
209
|
</div>
|
|
@@ -214,6 +216,7 @@ export function GitPanel({
|
|
|
214
216
|
)}
|
|
215
217
|
|
|
216
218
|
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
219
|
+
{!cwd && <div className="rounded-xl px-3 py-4 text-sm" style={{ color: "var(--dsw-label-caption)", background: "var(--dsw-hover)" }}>请先在工作台选择工作区,再查看 Git 差异。</div>}
|
|
217
220
|
{/* ---------- 差异视图 ---------- */}
|
|
218
221
|
{view && (
|
|
219
222
|
<div>
|
|
@@ -267,8 +270,7 @@ export function GitPanel({
|
|
|
267
270
|
{!error && info.files.length > 0 && onAskCommit && (
|
|
268
271
|
<button
|
|
269
272
|
type="button"
|
|
270
|
-
className="btn-
|
|
271
|
-
style={{ height: 28, padding: "0 12px", fontSize: 12 }}
|
|
273
|
+
className="clay-btn clay-btn-mint text-white font-semibold px-3.5 py-1.5 rounded-full text-xs self-start shadow-sm active:scale-95"
|
|
272
274
|
onClick={onAskCommit}
|
|
273
275
|
title={t.gitAskCommitPrompt}
|
|
274
276
|
>
|
|
@@ -21,6 +21,9 @@ export function ModelSelector({
|
|
|
21
21
|
thinkingLevel,
|
|
22
22
|
thinkingLevels,
|
|
23
23
|
models,
|
|
24
|
+
modelLoading = false,
|
|
25
|
+
modelLoadError,
|
|
26
|
+
onRetryModels,
|
|
24
27
|
providerNames,
|
|
25
28
|
authByProvider,
|
|
26
29
|
onSelectModel,
|
|
@@ -32,6 +35,9 @@ export function ModelSelector({
|
|
|
32
35
|
thinkingLevel?: string;
|
|
33
36
|
thinkingLevels: string[];
|
|
34
37
|
models: ModelChoice[];
|
|
38
|
+
modelLoading?: boolean;
|
|
39
|
+
modelLoadError?: string | null;
|
|
40
|
+
onRetryModels?: () => void;
|
|
35
41
|
providerNames: Record<string, string>;
|
|
36
42
|
authByProvider: Record<string, boolean>;
|
|
37
43
|
onSelectModel: (provider: string, id: string) => void;
|
|
@@ -102,7 +108,8 @@ export function ModelSelector({
|
|
|
102
108
|
});
|
|
103
109
|
}, [open, pane]);
|
|
104
110
|
|
|
105
|
-
|
|
111
|
+
// Pi uses this sentinel when no usable model can be restored.
|
|
112
|
+
const hasModel = Boolean(model?.id && !(model.provider === "unknown" && model.id === "unknown"));
|
|
106
113
|
const modelName = hasModel ? (model?.name || model?.id || "") : "";
|
|
107
114
|
const effortLabel = hasModel && thinkingLevel && thinkingLevel !== "off" ? thinkingLevel : undefined;
|
|
108
115
|
const triggerTitle = hasModel
|
|
@@ -110,10 +117,10 @@ export function ModelSelector({
|
|
|
110
117
|
: t.selectModel;
|
|
111
118
|
|
|
112
119
|
return (
|
|
113
|
-
<div ref={ref} className="relative">
|
|
120
|
+
<div ref={ref} className="pw-model-selector relative min-w-0">
|
|
114
121
|
<button
|
|
115
122
|
type="button"
|
|
116
|
-
className="chip"
|
|
123
|
+
className="chip min-w-0 max-w-full"
|
|
117
124
|
data-open={open}
|
|
118
125
|
title={triggerTitle}
|
|
119
126
|
aria-label={triggerTitle}
|
|
@@ -124,7 +131,7 @@ export function ModelSelector({
|
|
|
124
131
|
setPane("model");
|
|
125
132
|
}}
|
|
126
133
|
>
|
|
127
|
-
<span className="max-w-[220px] truncate" suppressHydrationWarning>{hasModel ? modelName : t.selectModel}</span>
|
|
134
|
+
<span className="min-w-0 max-w-[220px] truncate" suppressHydrationWarning>{hasModel ? modelName : t.selectModel}</span>
|
|
128
135
|
{effortLabel && (
|
|
129
136
|
<span style={{ color: "var(--dsw-label-caption)", flex: "none", fontSize: 13 }}>
|
|
130
137
|
{effortLabel}
|
|
@@ -198,9 +205,15 @@ export function ModelSelector({
|
|
|
198
205
|
{pane === "model" && (
|
|
199
206
|
<div className="flex min-h-0 flex-col">
|
|
200
207
|
<div ref={listRef} className="max-h-[340px] overflow-y-auto py-1">
|
|
201
|
-
{
|
|
208
|
+
{modelLoadError && (
|
|
209
|
+
<div role="alert" className="px-3 py-2" style={{ fontSize: 12, color: "var(--dsw-danger)" }}>
|
|
210
|
+
<span title={modelLoadError}>{t.modelsLoadFailed}</span>{" "}
|
|
211
|
+
{onRetryModels && <button type="button" className="underline" onClick={onRetryModels}>{t.settingsRetry}</button>}
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
{groups.length === 0 && (modelLoading || !modelLoadError) && (
|
|
202
215
|
<div className="px-3 py-3" style={{ fontSize: 13, color: "var(--dsw-label-caption)" }}>
|
|
203
|
-
{t.emptyModels}
|
|
216
|
+
{modelLoading ? t.modelsLoading : t.emptyModels}
|
|
204
217
|
</div>
|
|
205
218
|
)}
|
|
206
219
|
{groups.map((g) => (
|