@rexvane/piweb 0.3.3 → 0.3.4
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/package.json +1 -1
- package/scripts/build-output.mjs +3 -2
- package/src/app/api/models/route.ts +2 -1
- package/src/app/api/skills/route.ts +7 -2
- package/src/components/AppShell.tsx +49 -6
- package/src/components/ChatInput.tsx +5 -1
- package/src/components/ChatWindow.tsx +13 -3
- package/src/components/ContextMeter.tsx +74 -31
- package/src/components/ProjectPanel.tsx +6 -0
- package/src/components/ProviderSetupModal.module.css +18 -0
- package/src/components/ProviderSetupModal.tsx +20 -8
- package/src/components/SettingsPanel.tsx +72 -16
- package/src/hooks/useGrowth.ts +21 -7
- package/src/hooks/usePiWeb.ts +23 -0
- package/src/i18n.tsx +54 -2
- package/src/lib/agent-manager.ts +104 -1
- package/src/lib/growth-service.ts +93 -49
- package/src/lib/models-service.ts +53 -13
- package/src/lib/process-format.ts +49 -1
- package/src/lib/skills-service.ts +23 -0
- package/src/lib/types.ts +24 -0
- package/src/lib/update-service.ts +1 -1
- package/src/lib/workspace-store.ts +81 -43
package/package.json
CHANGED
package/scripts/build-output.mjs
CHANGED
|
@@ -54,8 +54,9 @@ export function assertInstallation(root) {
|
|
|
54
54
|
const realRoot = fs.realpathSync(root);
|
|
55
55
|
const manifestPath = assertLocalPath(realRoot, "package.json");
|
|
56
56
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
57
|
-
// Accept the historical
|
|
58
|
-
|
|
57
|
+
// Accept the historical names so installations made before the npm rename keep working.
|
|
58
|
+
const validNames = ["piweb", "pi-web", "@rexvane/piweb"];
|
|
59
|
+
if (!validNames.includes(manifest?.name)) throw new Error(`refusing to update outside the PiWeb installation: ${realRoot}`);
|
|
59
60
|
return realRoot;
|
|
60
61
|
}
|
|
61
62
|
|
|
@@ -72,6 +72,7 @@ export async function POST(req: Request) {
|
|
|
72
72
|
revision?: string;
|
|
73
73
|
text?: string;
|
|
74
74
|
force?: boolean;
|
|
75
|
+
allowPrivate?: boolean;
|
|
75
76
|
};
|
|
76
77
|
if (body.action === "setKey") {
|
|
77
78
|
if (!body.providerId || !body.apiKey) return NextResponse.json({ success: false, error: "missing fields" }, { status: 400 });
|
|
@@ -93,7 +94,7 @@ export async function POST(req: Request) {
|
|
|
93
94
|
}
|
|
94
95
|
if (body.action === "discoverModels") {
|
|
95
96
|
if (!body.baseUrl) return NextResponse.json({ success: false, error: "missing baseUrl" }, { status: 400 });
|
|
96
|
-
const models = await discoverModels({ baseUrl: body.baseUrl, api: body.api, apiKey: body.apiKey, providerId: body.providerId });
|
|
97
|
+
const models = await discoverModels({ baseUrl: body.baseUrl, api: body.api, apiKey: body.apiKey, providerId: body.providerId, allowPrivate: body.allowPrivate === true });
|
|
97
98
|
return NextResponse.json({ success: true, data: { models } });
|
|
98
99
|
}
|
|
99
100
|
if (body.action === "providerUsage") {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
-
import { listSkills, readSkillFile, setSkillDisabled } from "@/lib/skills-service";
|
|
2
|
+
import { deleteSkill, listSkills, readSkillFile, setSkillDisabled } from "@/lib/skills-service";
|
|
3
3
|
import { BoundaryError, resolveWorkspacePath } from "@/lib/path-security";
|
|
4
4
|
|
|
5
5
|
export const dynamic = "force-dynamic";
|
|
@@ -17,7 +17,7 @@ export async function GET(req: Request) {
|
|
|
17
17
|
|
|
18
18
|
export async function POST(req: Request) {
|
|
19
19
|
try {
|
|
20
|
-
const body = (await req.json()) as { action: "toggle" | "read"; filePath?: string; disabled?: boolean; cwd?: string };
|
|
20
|
+
const body = (await req.json()) as { action: "toggle" | "read" | "delete"; filePath?: string; disabled?: boolean; cwd?: string };
|
|
21
21
|
const cwd = await resolveWorkspacePath(body.cwd || process.cwd());
|
|
22
22
|
if (body.action === "toggle") {
|
|
23
23
|
if (!body.filePath || typeof body.disabled !== "boolean")
|
|
@@ -29,6 +29,11 @@ export async function POST(req: Request) {
|
|
|
29
29
|
if (!body.filePath) return NextResponse.json({ success: false, error: "missing filePath" }, { status: 400 });
|
|
30
30
|
return NextResponse.json({ success: true, data: { content: await readSkillFile(body.filePath, cwd) } });
|
|
31
31
|
}
|
|
32
|
+
if (body.action === "delete") {
|
|
33
|
+
if (!body.filePath) return NextResponse.json({ success: false, error: "missing filePath" }, { status: 400 });
|
|
34
|
+
const r = await deleteSkill(body.filePath, cwd);
|
|
35
|
+
return NextResponse.json({ success: true, data: r });
|
|
36
|
+
}
|
|
32
37
|
return NextResponse.json({ success: false, error: "unknown action" }, { status: 400 });
|
|
33
38
|
} catch (error) {
|
|
34
39
|
return NextResponse.json(
|
|
@@ -81,6 +81,7 @@ export function AppShell() {
|
|
|
81
81
|
setGroupBy,
|
|
82
82
|
setOrderBy,
|
|
83
83
|
addWorkspaceByPicker,
|
|
84
|
+
addWorkspaceByPath,
|
|
84
85
|
removeWorkspace,
|
|
85
86
|
refreshModels,
|
|
86
87
|
sendCommand,
|
|
@@ -201,11 +202,17 @@ export function AppShell() {
|
|
|
201
202
|
}, [currentPath]);
|
|
202
203
|
|
|
203
204
|
// 已知工作区列表(给 Hero 建议);启动不预选任何工作区,
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
205
|
+
// 未选择时输入框仍可用,发送会提示先选择工作区。
|
|
206
|
+
// 已删除工作区要过滤(与会话栏分组同口径):否则它下面残留的会话 cwd
|
|
207
|
+
// 会把刚删掉的工作区重新喂回顶部下拉。
|
|
208
|
+
const knownCwds = useMemo(() => {
|
|
209
|
+
const removed = new Set(removedWorkspaces.map((w) => w.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase()));
|
|
210
|
+
const keep = (cwd: string) => {
|
|
211
|
+
const norm = cwd.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
212
|
+
return Boolean(norm) && !removed.has(norm);
|
|
213
|
+
};
|
|
214
|
+
return Array.from(new Set([...addedWorkspaces, ...sessions.map((s) => s.cwd)].filter(keep)));
|
|
215
|
+
}, [addedWorkspaces, sessions, removedWorkspaces]);
|
|
209
216
|
|
|
210
217
|
// 模型目录派生值:数百个模型对象的映射只在目录变化时重算,
|
|
211
218
|
// 不能跟着每次 token 增量 / 3 秒轮询重跑(usePiWeb 的 setState 都会触发本组件渲染)
|
|
@@ -695,6 +702,7 @@ export function AppShell() {
|
|
|
695
702
|
providerNames={providerNames}
|
|
696
703
|
authByProvider={authByProvider}
|
|
697
704
|
addWorkspaceByPicker={addWorkspaceByPicker}
|
|
705
|
+
addWorkspaceByPath={addWorkspaceByPath}
|
|
698
706
|
heroModel={heroModel}
|
|
699
707
|
onSelectHeroModel={(provider, id) => {
|
|
700
708
|
setHeroModel({ provider, id });
|
|
@@ -825,7 +833,7 @@ export function AppShell() {
|
|
|
825
833
|
contextPercent={snapshot?.contextUsage?.percent ?? null}
|
|
826
834
|
contextTokens={snapshot?.contextUsage?.tokens ?? null}
|
|
827
835
|
contextWindow={snapshot?.contextUsage?.contextWindow ?? null}
|
|
828
|
-
contextSource={{ systemChars: contextSystemChars, messages: state.messages }}
|
|
836
|
+
contextSource={{ systemChars: contextSystemChars, messages: state.messages, breakdown: snapshot?.contextBreakdown }}
|
|
829
837
|
contextVisible={(snapshot?.messages?.length ?? 0) > 0}
|
|
830
838
|
model={snapshot?.model}
|
|
831
839
|
thinkingLevel={snapshot?.thinkingLevel}
|
|
@@ -1013,6 +1021,7 @@ function Hero({
|
|
|
1013
1021
|
providerNames,
|
|
1014
1022
|
authByProvider,
|
|
1015
1023
|
addWorkspaceByPicker,
|
|
1024
|
+
addWorkspaceByPath,
|
|
1016
1025
|
heroModel,
|
|
1017
1026
|
onSelectHeroModel,
|
|
1018
1027
|
defaultModel,
|
|
@@ -1039,6 +1048,7 @@ function Hero({
|
|
|
1039
1048
|
providerNames: Record<string, string>;
|
|
1040
1049
|
authByProvider: Record<string, boolean>;
|
|
1041
1050
|
addWorkspaceByPicker: () => Promise<string | null>;
|
|
1051
|
+
addWorkspaceByPath: (dir: string) => Promise<string | null>;
|
|
1042
1052
|
heroModel: { provider: string; id: string } | null;
|
|
1043
1053
|
onSelectHeroModel: (provider: string, id: string) => void;
|
|
1044
1054
|
defaultModel?: ModelChoice;
|
|
@@ -1048,6 +1058,8 @@ function Hero({
|
|
|
1048
1058
|
}) {
|
|
1049
1059
|
const { t } = useI18n();
|
|
1050
1060
|
const [wsMenu, setWsMenu] = useState(false);
|
|
1061
|
+
const [wsPathDraft, setWsPathDraft] = useState("");
|
|
1062
|
+
const [wsPathError, setWsPathError] = useState<string | null>(null);
|
|
1051
1063
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
1052
1064
|
|
|
1053
1065
|
useEffect(() => {
|
|
@@ -1069,6 +1081,19 @@ function Hero({
|
|
|
1069
1081
|
}
|
|
1070
1082
|
};
|
|
1071
1083
|
|
|
1084
|
+
// 手动输入路径添加(原生选择器不可用 / 无桌面环境兜底)
|
|
1085
|
+
const addByPath = async () => {
|
|
1086
|
+
const p = await addWorkspaceByPath(wsPathDraft);
|
|
1087
|
+
if (p) {
|
|
1088
|
+
setWsPathDraft("");
|
|
1089
|
+
setWsPathError(null);
|
|
1090
|
+
setCwd(p);
|
|
1091
|
+
setWsMenu(false);
|
|
1092
|
+
} else {
|
|
1093
|
+
setWsPathError(t.workspacePathInvalid);
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1072
1097
|
const currentLabel = cwd ? (getWorkspaceName ? getWorkspaceName(cwd) : basename(cwd)) : t.startWith;
|
|
1073
1098
|
|
|
1074
1099
|
return (
|
|
@@ -1130,6 +1155,24 @@ function Hero({
|
|
|
1130
1155
|
<IconProjectAddOutline16 size={14} style={{ flex: "none", color: "var(--dsw-label-tertiary)" }} />
|
|
1131
1156
|
{t.addWorkspace}
|
|
1132
1157
|
</button>
|
|
1158
|
+
<div className="flex items-center gap-1.5 px-2 py-1.5" style={{ fontSize: 12.5 }}>
|
|
1159
|
+
<input
|
|
1160
|
+
className="min-w-0 flex-1 rounded-lg px-2 py-1"
|
|
1161
|
+
style={{ border: "0.5px solid var(--dsw-border-l3)", background: "var(--dsw-input-bg, transparent)", color: "inherit" }}
|
|
1162
|
+
placeholder={t.workspacePathPlaceholder}
|
|
1163
|
+
value={wsPathDraft}
|
|
1164
|
+
onChange={(e) => { setWsPathDraft(e.target.value); setWsPathError(null); }}
|
|
1165
|
+
onKeyDown={(e) => {
|
|
1166
|
+
if (e.key === "Enter") { e.preventDefault(); void addByPath(); }
|
|
1167
|
+
}}
|
|
1168
|
+
/>
|
|
1169
|
+
<button type="button" className="pw-chip" style={{ flex: "none" }} disabled={!wsPathDraft.trim()} onClick={() => void addByPath()}>
|
|
1170
|
+
{t.workspacePathAdd}
|
|
1171
|
+
</button>
|
|
1172
|
+
</div>
|
|
1173
|
+
{wsPathError && (
|
|
1174
|
+
<div className="px-3.5 pb-1.5" style={{ fontSize: 11, color: "var(--dsw-danger)" }}>{wsPathError}</div>
|
|
1175
|
+
)}
|
|
1133
1176
|
</div>
|
|
1134
1177
|
)}
|
|
1135
1178
|
</div>
|
|
@@ -84,7 +84,11 @@ export function ChatInput({
|
|
|
84
84
|
contextTokens: number | null;
|
|
85
85
|
contextWindow: number | null;
|
|
86
86
|
/** 上下文分段数据源:ContextMeter 弹窗打开时才做字符统计 */
|
|
87
|
-
contextSource?: {
|
|
87
|
+
contextSource?: {
|
|
88
|
+
systemChars: number;
|
|
89
|
+
messages: { role?: string; content: Array<{ type: string; text?: string; thinking?: string }> }[];
|
|
90
|
+
breakdown?: import("@/lib/types").ContextBreakdown;
|
|
91
|
+
};
|
|
88
92
|
/** 还没有对话时隐藏上下文计量 */
|
|
89
93
|
contextVisible?: boolean;
|
|
90
94
|
model?: { provider: string; id: string; name: string };
|
|
@@ -35,8 +35,10 @@ import type { ContextResource, TrajEntry, TrajTokens, WebContent, WebMessage, We
|
|
|
35
35
|
import {
|
|
36
36
|
classifyModelError,
|
|
37
37
|
cleanCommand,
|
|
38
|
+
deleteTargetOf,
|
|
38
39
|
displayToolName,
|
|
39
40
|
firstSentence,
|
|
41
|
+
isDeleteCommand,
|
|
40
42
|
langOfPath,
|
|
41
43
|
previewSide,
|
|
42
44
|
relativizeInText,
|
|
@@ -457,10 +459,10 @@ const ToolStep = memo(function ToolStep({
|
|
|
457
459
|
if (kind !== "write" || !state || state.state !== "done" || state.isError) return null;
|
|
458
460
|
const parsed = parseUnifiedDiff(state.patch || output);
|
|
459
461
|
if (parsed) return parsed;
|
|
460
|
-
// pi 的 write 工具不带 patch
|
|
462
|
+
// pi 的 write 工具不带 patch:新文件按新增行展示(全绿 +),与 edit 的 diff 口径一致
|
|
461
463
|
if (isWriteTool) {
|
|
462
464
|
const content = str((args as { content?: unknown } | undefined)?.content);
|
|
463
|
-
if (content) return content.replace(/\r/g, "").replace(/\n$/, "").split("\n").map((text, i): DiffLine => ({ kind: "
|
|
465
|
+
if (content) return content.replace(/\r/g, "").replace(/\n$/, "").split("\n").map((text, i): DiffLine => ({ kind: "add", new: i + 1, text }));
|
|
464
466
|
}
|
|
465
467
|
return null;
|
|
466
468
|
}, [args, isWriteTool, kind, output, state]);
|
|
@@ -478,6 +480,8 @@ const ToolStep = memo(function ToolStep({
|
|
|
478
480
|
return str(a?.path ?? a?.file_path) ?? "";
|
|
479
481
|
})();
|
|
480
482
|
const fullCommand = kind === "cmd" ? (str((args as Record<string, unknown>)?.command) ?? str((args as Record<string, unknown>)?.cmd) ?? str((args as Record<string, unknown>)?.script) ?? "") : "";
|
|
483
|
+
// 删除文件的命令:完成后给红色标记(内容级 diff 不强求)
|
|
484
|
+
const deletedTarget = kind === "cmd" && !running && !failed && isDeleteCommand(fullCommand) ? deleteTargetOf(fullCommand) : "";
|
|
481
485
|
const preview = state ? toolPreview(kind, state, tt, cwd) : { lines: [], hidden: 0 };
|
|
482
486
|
const hasDiff = Boolean(diff && diff.length) && !failed;
|
|
483
487
|
const hasOutput = Boolean(output || state?.encodingLoss);
|
|
@@ -488,7 +492,7 @@ const ToolStep = memo(function ToolStep({
|
|
|
488
492
|
const diffHidden = diff && diffLines && !open ? diff.length - diffLines.length : 0;
|
|
489
493
|
const diffStat = hasDiff && diff
|
|
490
494
|
? isWriteTool && !state?.patch
|
|
491
|
-
? t.
|
|
495
|
+
? t.diffSummary.replace("{add}", String(diff.length)).replace("{del}", "0")
|
|
492
496
|
: t.diffSummary.replace("{add}", String(diff.filter((l) => l.kind === "add").length)).replace("{del}", String(diff.filter((l) => l.kind === "del").length))
|
|
493
497
|
: "";
|
|
494
498
|
const toggle = () => setOpen((o) => !o);
|
|
@@ -508,6 +512,11 @@ const ToolStep = memo(function ToolStep({
|
|
|
508
512
|
<span className="pw-arg" style={{ color: "var(--dsw-label-caption)" }}>{displayToolName(name)}</span>
|
|
509
513
|
)}
|
|
510
514
|
{diffStat && <span className="pw-meta">{diffStat}</span>}
|
|
515
|
+
{deletedTarget && (
|
|
516
|
+
<span className="pw-meta" style={{ color: "var(--dsw-danger)" }} title={deletedTarget}>
|
|
517
|
+
{t.deletedFile.replace("{path}", relativizeInText(deletedTarget, cwd || ""))}
|
|
518
|
+
</span>
|
|
519
|
+
)}
|
|
511
520
|
{summary.scriptLines > 1 && <span className="pw-meta">{t.commandLines.replace("{n}", String(summary.scriptLines))}</span>}
|
|
512
521
|
{running && <span className="pw-meta">{fmtSpan(elapsed)}</span>}
|
|
513
522
|
{!running && duration >= 1000 && <span className="pw-meta">{fmtSpan(duration)}</span>}
|
|
@@ -638,6 +647,7 @@ const ERROR_HINT_KEY: Record<ModelErrorKind, string> = {
|
|
|
638
647
|
timeout: "modelErrorTimeout",
|
|
639
648
|
server: "modelErrorServer",
|
|
640
649
|
network: "modelErrorNetwork",
|
|
650
|
+
moderation: "modelErrorModeration",
|
|
641
651
|
};
|
|
642
652
|
|
|
643
653
|
function ErrorStep({ message, count, model }: { message: string; count: number; model?: string }) {
|
|
@@ -1,18 +1,31 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* 圆环常驻按钮 + 弹窗显示「百分比 / ~已用 /
|
|
6
|
-
*
|
|
4
|
+
* 上下文占用:
|
|
5
|
+
* 圆环常驻按钮 + 弹窗显示「百分比 / ~已用 / 窗口」与 13 类分段占用条
|
|
6
|
+
* (mac 磁盘存储条式的多色分段,含 Free Space 段)。
|
|
7
|
+
* 分段口径:全部为字符/4 启发式估算(与 SDK estimateTokens 一致),
|
|
8
|
+
* 服务端分项(System Prompt / Tools / Memory / Skills / Compacted / Buffer)来自快照,
|
|
9
|
+
* 消息分项(User / Agent Text / Thinking / Tool Call / Tool Output)弹窗打开时前端统计。
|
|
7
10
|
* 无对话时不渲染(由父组件经 contextVisible 控制)。
|
|
8
11
|
*/
|
|
9
12
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
10
13
|
import { useI18n } from "@/i18n";
|
|
14
|
+
import { classifyMessageChars } from "@/lib/process-format";
|
|
15
|
+
import type { ContextBreakdown } from "@/lib/types";
|
|
11
16
|
|
|
12
17
|
function fmtTok(n: number): string {
|
|
13
18
|
return n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(Math.round(n));
|
|
14
19
|
}
|
|
15
20
|
|
|
21
|
+
/** 13 类分段的定义(顺序即渲染顺序) */
|
|
22
|
+
interface Segment {
|
|
23
|
+
key: string;
|
|
24
|
+
label: string;
|
|
25
|
+
tokens: number;
|
|
26
|
+
color: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
16
29
|
export function ContextMeter({
|
|
17
30
|
percent,
|
|
18
31
|
tokens,
|
|
@@ -22,8 +35,12 @@ export function ContextMeter({
|
|
|
22
35
|
percent: number | null;
|
|
23
36
|
tokens: number | null;
|
|
24
37
|
contextWindow: number | null;
|
|
25
|
-
/**
|
|
26
|
-
source?: {
|
|
38
|
+
/** 分段数据源:消息字符统计只在弹窗打开时执行(流式期间消息每 token 都变,不能常驻扫描) */
|
|
39
|
+
source?: {
|
|
40
|
+
systemChars: number;
|
|
41
|
+
messages: { role?: string; content: Array<{ type: string; text?: string; thinking?: string; arguments?: unknown }> }[];
|
|
42
|
+
breakdown?: ContextBreakdown;
|
|
43
|
+
};
|
|
27
44
|
}) {
|
|
28
45
|
const [open, setOpen] = useState(false);
|
|
29
46
|
const ref = useRef<HTMLDivElement>(null);
|
|
@@ -38,16 +55,21 @@ export function ContextMeter({
|
|
|
38
55
|
return () => document.removeEventListener("mousedown", h);
|
|
39
56
|
}, [open]);
|
|
40
57
|
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
const used = tokens ?? 0;
|
|
59
|
+
const window_ = contextWindow ?? 0;
|
|
60
|
+
const b = source?.breakdown;
|
|
61
|
+
|
|
62
|
+
// 消息分项只在弹窗打开时统计(≈4 字符/token,与 SDK 一致)
|
|
63
|
+
const messageSegments = useMemo(() => {
|
|
64
|
+
if (!open) return { user: 0, agentText: 0, agentThinking: 0, agentToolCall: 0, toolOutput: 0 };
|
|
65
|
+
const chars = classifyMessageChars(source?.messages ?? []);
|
|
66
|
+
return {
|
|
67
|
+
user: Math.max(0, Math.round(chars.user / 4)),
|
|
68
|
+
agentText: Math.max(0, Math.round(chars.agentText / 4)),
|
|
69
|
+
agentThinking: Math.max(0, Math.round(chars.agentThinking / 4)),
|
|
70
|
+
agentToolCall: Math.max(0, Math.round(chars.agentToolCall / 4)),
|
|
71
|
+
toolOutput: Math.max(0, Math.round(chars.toolOutput / 4)),
|
|
72
|
+
};
|
|
51
73
|
}, [open, source]);
|
|
52
74
|
|
|
53
75
|
const p = percent ?? 0;
|
|
@@ -55,15 +77,34 @@ export function ContextMeter({
|
|
|
55
77
|
const r = 5.5;
|
|
56
78
|
const c = 2 * Math.PI * r;
|
|
57
79
|
const shown = Math.max(0, Math.min(100, p));
|
|
58
|
-
const used = tokens ?? 0;
|
|
59
|
-
const window_ = contextWindow ?? 0;
|
|
60
80
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
81
|
+
const segments: Segment[] = b
|
|
82
|
+
? [
|
|
83
|
+
{ key: "systemPrompt", label: t.contextSystemPrompt, tokens: b.systemPrompt, color: "var(--dsw-accent)" },
|
|
84
|
+
{ key: "systemTools", label: t.contextSystemTools, tokens: b.systemTools, color: "#8B7EC8" },
|
|
85
|
+
{ key: "customTools", label: t.contextCustomTools, tokens: b.customTools, color: "#B39DDB" },
|
|
86
|
+
{ key: "memory", label: t.contextMemory, tokens: b.memory, color: "#5FA8D3" },
|
|
87
|
+
{ key: "skills", label: t.contextSkills, tokens: b.skills, color: "#4FB286" },
|
|
88
|
+
{ key: "user", label: t.contextUserMessages, tokens: messageSegments.user, color: "var(--dsw-success)" },
|
|
89
|
+
{ key: "agentText", label: t.contextAgentText, tokens: messageSegments.agentText, color: "#7BC96F" },
|
|
90
|
+
{ key: "agentThinking", label: t.contextAgentThinking, tokens: messageSegments.agentThinking, color: "#C9A66B" },
|
|
91
|
+
{ key: "agentToolCall", label: t.contextAgentToolCall, tokens: messageSegments.agentToolCall, color: "#D3A15F" },
|
|
92
|
+
{ key: "toolOutput", label: t.contextToolOutput, tokens: messageSegments.toolOutput, color: "var(--dsw-warn)" },
|
|
93
|
+
{ 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
|
+
]
|
|
96
|
+
: // 服务端分项缺失(旧快照):退回旧三段口径
|
|
97
|
+
[
|
|
98
|
+
{ key: "system", label: t.contextSystem, tokens: Math.max(0, Math.round((source?.systemChars ?? 0) / 4)), color: "var(--dsw-accent)" },
|
|
99
|
+
{ key: "messages", label: t.contextMessages, tokens: messageSegments.user + messageSegments.agentText + messageSegments.agentThinking, color: "var(--dsw-success)" },
|
|
100
|
+
{ 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
|
+
];
|
|
102
|
+
|
|
103
|
+
// Free Space:窗口余量(窗口未知则不显示);分段条不归一化到已用之和
|
|
104
|
+
const usedSum = segments.reduce((sum, s) => sum + s.tokens, 0);
|
|
105
|
+
const free = window_ > 0 ? Math.max(0, window_ - usedSum) : 0;
|
|
106
|
+
const barTotal = window_ > 0 ? window_ : usedSum || 1;
|
|
107
|
+
const bar = (n: number) => `${(n / barTotal) * 100}%`;
|
|
67
108
|
|
|
68
109
|
return (
|
|
69
110
|
<div ref={ref} className="relative">
|
|
@@ -98,18 +139,20 @@ export function ContextMeter({
|
|
|
98
139
|
{tokens === null ? "—" : `~${fmtTok(used)}`} / {window_ > 0 ? fmtTok(window_) : "—"}
|
|
99
140
|
</div>
|
|
100
141
|
|
|
101
|
-
{/* 分段占用条(mac
|
|
102
|
-
{
|
|
142
|
+
{/* 分段占用条(mac 磁盘存储条式多色分段,含 Free Space) */}
|
|
143
|
+
{usedSum > 0 && (
|
|
103
144
|
<div className="mt-3 flex h-2 w-full overflow-hidden rounded-full" style={{ background: "var(--dsw-selector)" }} aria-hidden>
|
|
104
|
-
{
|
|
105
|
-
|
|
106
|
-
|
|
145
|
+
{segments.filter((s) => s.tokens > 0).map((s) => (
|
|
146
|
+
<span key={s.key} style={{ width: bar(s.tokens), background: s.color }} />
|
|
147
|
+
))}
|
|
148
|
+
{free > 0 && <span style={{ width: bar(free), background: "transparent" }} />}
|
|
107
149
|
</div>
|
|
108
150
|
)}
|
|
109
151
|
<div className="mt-2 flex flex-col gap-1" style={{ color: "var(--dsw-label-tertiary)" }}>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
152
|
+
{segments.filter((s) => s.tokens > 0).map((s) => (
|
|
153
|
+
<SegmentRow key={s.key} label={s.label} tokens={s.tokens} color={s.color} />
|
|
154
|
+
))}
|
|
155
|
+
{window_ > 0 && <SegmentRow label={t.contextFreeSpace} tokens={free} color="var(--dsw-selector)" />}
|
|
113
156
|
</div>
|
|
114
157
|
</div>
|
|
115
158
|
)}
|
|
@@ -161,6 +161,12 @@ export function ProjectPanel({
|
|
|
161
161
|
<IconCloseOutline14 size={13} />
|
|
162
162
|
</button>
|
|
163
163
|
</div>
|
|
164
|
+
{/* 生长快照不可用(找不到 git 等):明确提示 + 已退化为当前目录结构 */}
|
|
165
|
+
{cwd && hasSession && !growth.available && !growth.loading && (
|
|
166
|
+
<div className="mx-3 mb-2 rounded-lg px-2 py-1" style={{ fontSize: 11.5, color: "var(--dsw-warn)", background: "var(--dsw-hover)" }} role="status">
|
|
167
|
+
{t.growthUnavailableFallback}
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
164
170
|
{gitError && (
|
|
165
171
|
<div className="mx-3 mb-2 rounded-lg px-2 py-1" style={{ fontSize: 11.5, color: "var(--dsw-danger)" }} title={gitError} role="alert" data-testid="growth-git-error">
|
|
166
172
|
{t.gitStatusUnknown}
|
|
@@ -271,6 +271,24 @@
|
|
|
271
271
|
opacity: 0.48;
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
.discoverControls {
|
|
275
|
+
display: flex;
|
|
276
|
+
flex: none;
|
|
277
|
+
align-items: center;
|
|
278
|
+
gap: 12px;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.allowPrivate {
|
|
282
|
+
display: flex;
|
|
283
|
+
align-items: center;
|
|
284
|
+
gap: 4px;
|
|
285
|
+
font-size: 11.5px;
|
|
286
|
+
color: var(--dsw-label-caption);
|
|
287
|
+
cursor: pointer;
|
|
288
|
+
user-select: none;
|
|
289
|
+
white-space: nowrap;
|
|
290
|
+
}
|
|
291
|
+
|
|
274
292
|
.modelEmpty {
|
|
275
293
|
margin-top: 13px;
|
|
276
294
|
padding: 17px;
|
|
@@ -145,13 +145,13 @@ export function ProviderSetupModal({
|
|
|
145
145
|
};
|
|
146
146
|
|
|
147
147
|
/** 供 ModelCatalog 的候选对话框调用:用表单当前值(含未保存的 key)探测远端目录 */
|
|
148
|
-
const discoverAvailableModels = async (): Promise<ModelDraft[]> => {
|
|
148
|
+
const discoverAvailableModels = async (allowPrivate: boolean): Promise<ModelDraft[]> => {
|
|
149
149
|
if (!baseUrl.trim()) throw new Error(t.apiAddress);
|
|
150
150
|
const response = await fetch("/api/models", {
|
|
151
151
|
method: "POST",
|
|
152
152
|
headers: { "Content-Type": "application/json" },
|
|
153
153
|
// builtin 模式带 providerId:表单 key 为空时后端回退到已存密钥 / OAuth 令牌
|
|
154
|
-
body: JSON.stringify({ action: "discoverModels", baseUrl: baseUrl.trim(), api: mode === "builtin" ? selected?.apis[0] : api, apiKey: apiKey.trim(), providerId: mode === "builtin" ? selected?.id : undefined }),
|
|
154
|
+
body: JSON.stringify({ action: "discoverModels", baseUrl: baseUrl.trim(), api: mode === "builtin" ? selected?.apis[0] : api, apiKey: apiKey.trim(), providerId: mode === "builtin" ? selected?.id : undefined, allowPrivate }),
|
|
155
155
|
});
|
|
156
156
|
const result = await response.json();
|
|
157
157
|
if (!response.ok || !result.success) throw new Error(result.error || t.toastError);
|
|
@@ -332,14 +332,16 @@ export function ModelCatalog({
|
|
|
332
332
|
setModels: React.Dispatch<React.SetStateAction<ModelDraft[]>>;
|
|
333
333
|
updateModel: (index: number, patch: Partial<ModelDraft>) => void;
|
|
334
334
|
inheritedCount?: number;
|
|
335
|
-
/**
|
|
336
|
-
discover?: () => Promise<ModelDraft[]>;
|
|
335
|
+
/** 用表单当前值探测远端目录(allowPrivate:用户勾选了允许访问本机/私网地址);抛错时错误显示在目录内 */
|
|
336
|
+
discover?: (allowPrivate: boolean) => Promise<ModelDraft[]>;
|
|
337
337
|
discoverDisabled?: boolean;
|
|
338
338
|
}) {
|
|
339
339
|
const { t } = useI18n();
|
|
340
340
|
const [expanded, setExpanded] = useState<ReadonlySet<number>>(new Set());
|
|
341
341
|
const [discovering, setDiscovering] = useState(false);
|
|
342
342
|
const [discoverError, setDiscoverError] = useState("");
|
|
343
|
+
// 本地网关(Ollama 等)在私网/回环地址上,探测需要用户显式授权
|
|
344
|
+
const [allowPrivate, setAllowPrivate] = useState(false);
|
|
343
345
|
// 候选对话框:null = 关闭
|
|
344
346
|
const [candidates, setCandidates] = useState<ModelDraft[] | null>(null);
|
|
345
347
|
const [picked, setPicked] = useState<ReadonlySet<string>>(new Set());
|
|
@@ -384,7 +386,7 @@ export function ModelCatalog({
|
|
|
384
386
|
setDiscovering(true);
|
|
385
387
|
setDiscoverError("");
|
|
386
388
|
try {
|
|
387
|
-
const found = await discover();
|
|
389
|
+
const found = await discover(allowPrivate);
|
|
388
390
|
if (!found.length) {
|
|
389
391
|
setDiscoverError(t.discoverEmpty);
|
|
390
392
|
return;
|
|
@@ -441,9 +443,19 @@ export function ModelCatalog({
|
|
|
441
443
|
</div>
|
|
442
444
|
</div>
|
|
443
445
|
{discover ? (
|
|
444
|
-
<
|
|
445
|
-
{
|
|
446
|
-
|
|
446
|
+
<div className={styles.discoverControls}>
|
|
447
|
+
<label className={styles.allowPrivate} title={t.allowPrivateHint}>
|
|
448
|
+
<input
|
|
449
|
+
type="checkbox"
|
|
450
|
+
checked={allowPrivate}
|
|
451
|
+
onChange={(event) => setAllowPrivate(event.target.checked)}
|
|
452
|
+
/>
|
|
453
|
+
{t.allowPrivateDiscovery}
|
|
454
|
+
</label>
|
|
455
|
+
<button className={styles.discoverButton} disabled={discoverDisabled || discovering} onClick={() => void startDiscover()}>
|
|
456
|
+
{discovering ? t.discoveringModels : t.discoverModels}
|
|
457
|
+
</button>
|
|
458
|
+
</div>
|
|
447
459
|
) : null}
|
|
448
460
|
</div>
|
|
449
461
|
{discoverError ? <div className={styles.catalogError}>{discoverError}</div> : null}
|