Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .localappdata/npm-cache/_logs/2026-09-04T01_11_05_158Z-debug-0.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
0 verbose cli D:\softwareDev\nodejs\node.exe D:\softwareDev\nodejs\node_modules\npm\bin\npm-cli.js
1 info using npm@10.9.3
2 info using node@v22.19.0
3 silly config load:file:D:\softwareDev\nodejs\node_modules\npm\npmrc
4 silly config load:file:D:\softwareDev\pi_web\pi-web-main\.npmrc
5 silly config load:file:C:\Users\Administrator\.npmrc
6 silly config load:file:D:\phpstudy_pro\ai模型中转站\sub2api-main\tools\npm-global\etc\npmrc
7 verbose title npm run build
8 verbose argv "run" "build"
9 verbose logfile logs-max:10 dir:D:\softwareDev\pi_web\pi-web-main\.localappdata\npm-cache\_logs\2026-09-04T01_11_05_158Z-
10 verbose logfile D:\softwareDev\pi_web\pi-web-main\.localappdata\npm-cache\_logs\2026-09-04T01_11_05_158Z-debug-0.log
11 silly logfile done cleaning log files
12 verbose cwd D:\softwareDev\pi_web\pi-web-main
13 verbose os Windows_NT 10.0.17763
14 verbose node v22.19.0
15 verbose npm v10.9.3
16 verbose exit 1
17 verbose code 1
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1 change: 0 additions & 1 deletion app/globals.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
@import "@xterm/xterm/css/xterm.css";
@import "tailwindcss";

@theme {
Expand Down
1 change: 1 addition & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata, Viewport } from "next";
import { Noto_Sans_Mono } from "next/font/google";
import { PwaRegistration } from "@/components/PwaRegistration";
import "katex/dist/katex.min.css";
import "@xterm/xterm/css/xterm.css";
import "./globals.css";
import "./settings.css";

Expand Down
Empty file modified bin/pi-web.js
100755 → 100644
Empty file.
61 changes: 61 additions & 0 deletions components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ interface Props {
onCompact?: () => void;
onAbortCompaction?: () => void;
isCompacting?: boolean;
onWorkspaceHistoryCommand?: (command: "undo" | "redo" | "checkpoint") => void;
compactError?: string | null;
compactResult?: CompactResultInfo | null;
toolPreset?: ToolPreset;
Expand Down Expand Up @@ -473,6 +474,7 @@ export function ModelScopeWarningBanner({ warnings }: { warnings?: string[] }) {
export const ChatInput = forwardRef<ChatInputHandle, Props>(function ChatInput({
onSend, onAbort, onSteer, onFollowUp, isStreaming, model, isAutoModelSelection, modelNames, modelList, modelError, modelScopeWarnings, onModelChange, modelSwitching,
onCompact, onAbortCompaction, isCompacting, compactError, compactResult, toolPreset, onToolPresetChange,
onWorkspaceHistoryCommand,
thinkingLevel, onThinkingLevelChange, availableThinkingLevels, thinkingLevelMap,
retryInfo, queuedMessages, inputHistory = [], onRecallQueue,
slashCommands, slashCommandsLoading, onLoadSlashCommands,
Expand Down Expand Up @@ -2482,6 +2484,65 @@ export const ChatInput = forwardRef<ChatInputHandle, Props>(function ChatInput({
</div>
)}

{onWorkspaceHistoryCommand && !isStreaming && (
<div style={{ display: "flex", alignItems: "center", gap: 2 }}>
<button
onClick={() => onWorkspaceHistoryCommand("undo")}
title={t("chat.undo")}
aria-label={t("chat.undo")}
style={{
display: "flex", alignItems: "center", justifyContent: "center",
width: 32, height: 32, padding: 0,
background: "none", border: "none", borderRadius: 9,
color: "var(--text-muted)", cursor: "pointer",
transition: "background 0.12s, color 0.12s",
}}
onMouseEnter={(e) => { e.currentTarget.style.background = "var(--bg-hover)"; e.currentTarget.style.color = "var(--text)"; }}
onMouseLeave={(e) => { e.currentTarget.style.background = "none"; e.currentTarget.style.color = "var(--text-muted)"; }}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="1 4 1 10 7 10" /><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
</svg>
</button>
<button
onClick={() => onWorkspaceHistoryCommand("redo")}
title={t("chat.redo")}
aria-label={t("chat.redo")}
style={{
display: "flex", alignItems: "center", justifyContent: "center",
width: 32, height: 32, padding: 0,
background: "none", border: "none", borderRadius: 9,
color: "var(--text-muted)", cursor: "pointer",
transition: "background 0.12s, color 0.12s",
}}
onMouseEnter={(e) => { e.currentTarget.style.background = "var(--bg-hover)"; e.currentTarget.style.color = "var(--text)"; }}
onMouseLeave={(e) => { e.currentTarget.style.background = "none"; e.currentTarget.style.color = "var(--text-muted)"; }}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="23 4 23 10 17 10" /><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
</svg>
</button>
<button
onClick={() => onWorkspaceHistoryCommand("checkpoint")}
title={t("chat.checkpoint")}
aria-label={t("chat.checkpoint")}
style={{
display: "flex", alignItems: "center", justifyContent: "center",
width: 32, height: 32, padding: 0,
background: "none", border: "none", borderRadius: 9,
color: "var(--text-muted)", cursor: "pointer",
transition: "background 0.12s, color 0.12s",
}}
onMouseEnter={(e) => { e.currentTarget.style.background = "var(--bg-hover)"; e.currentTarget.style.color = "var(--text)"; }}
onMouseLeave={(e) => { e.currentTarget.style.background = "none"; e.currentTarget.style.color = "var(--text-muted)"; }}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
</svg>
</button>
</div>
)}

{isStreaming && (
<button
onClick={onAbort}
Expand Down
7 changes: 6 additions & 1 deletion components/ChatWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ function phaseLabel(phase: AgentPhase, t: (key: string, params?: Record<string,
}
if (phase?.kind === "waiting_model") return t("chat.waitingModel");
if (phase?.kind === "running_command") return t("chat.runningCommand");
if (phase?.kind === "compacting") return t(phase.auto ? "chat.autoCompacting" : "chat.compacting");
return null;
}

Expand Down Expand Up @@ -295,6 +296,9 @@ export function ChatWindow({ session, searchTarget, onSearchTargetHandled, initi
deferInitialScroll: Boolean(pendingScrollRestore),
});
const sessionBusy = agentRunning || bashRunning;
const handleWorkspaceHistoryCommand = useCallback((command: "undo" | "redo" | "checkpoint") => {
handleSend(`/${command}`);
}, [handleSend]);
const [quotedSelection, setQuotedSelection] = useState<{
text: string;
top: number;
Expand Down Expand Up @@ -896,6 +900,7 @@ export function ChatWindow({ session, searchTarget, onSearchTargetHandled, initi
onAudioUnlock={unlockAudio}
draftKey={session?.id ?? newSessionDraftKey ?? undefined}
cwd={session?.cwd ?? newSessionCwd}
onWorkspaceHistoryCommand={handleWorkspaceHistoryCommand}
/>
);

Expand Down Expand Up @@ -1025,7 +1030,7 @@ export function ChatWindow({ session, searchTarget, onSearchTargetHandled, initi
const isVisible = isMessageGroupAnchor(msg) || msg.role === "assistant";
const currentRefIdx = visibleRefIndexByMessage.get(idx);
const keyPrefix = options.keyPrefix ?? "message";
const messageKey = entryIds[idx] ?? idx;
const messageKey = entryIds[idx] ?? `i${idx}`;
let showTimestamp = false;
if (msg.role === "assistant") {
showTimestamp = true;
Expand Down
40 changes: 37 additions & 3 deletions components/MessageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1004,11 +1004,24 @@ function isSubagentToolDetails(value: unknown): value is SubagentToolDetails {
function ToolCallBlock({ block, result, duration, onOpenSession }: { block: ToolCallContent; result?: ToolResultMessage; duration?: number; onOpenSession?: (sessionId: string) => void }) {
const { t } = useI18n();
const [expanded, setExpanded] = useState(false);
const [copied, setCopied] = useState(false);
const inputStr = getToolCallInputText(block);
const isStreamingInput = block.rawInput !== undefined;
const isEditTool = isEditToolName(block.toolName);
const resultDiff = result && !result.isError ? getResultDiff(result) : null;

// 复制工具输入的命令(如 bash 命令),而非工具的回复内容
const copyCommand = block.input && typeof block.input === "object" && "command" in block.input
? String((block.input as { command: unknown }).command)
: null;

const handleCopy = () => {
copyText(copyCommand ?? inputStr).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
});
};

// Result display
const resultText = result
? result.content.filter((b): b is { type: "text"; text: string } => b.type === "text").map((b) => b.text).join("\n")
Expand Down Expand Up @@ -1056,7 +1069,26 @@ function ToolCallBlock({ block, result, duration, onOpenSession }: { block: Tool
{duration !== undefined && (
<span style={{ fontSize: 11, color: "var(--text-dim)", flexShrink: 0, fontVariantNumeric: "tabular-nums" }}>{duration}s</span>
)}
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="var(--text-dim)" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0, transform: expanded ? "rotate(180deg)" : "none", transition: "transform 0.15s" }}>
</button>
<button
type="button"
onClick={handleCopy}
title={copied ? t("i18n.copied") : t("i18n.copy")}
aria-label={copied ? t("i18n.copied") : t("i18n.copy")}
style={{ width: 32, display: "grid", placeItems: "center", border: "none", borderLeft: "1px solid var(--border)", background: "none", color: copied ? "var(--accent)" : "var(--text-muted)", cursor: "pointer", flexShrink: 0, transition: "color 0.12s" }}
onMouseEnter={(e) => { if (!copied) e.currentTarget.style.color = "var(--text)"; }}
onMouseLeave={(e) => { if (!copied) e.currentTarget.style.color = "var(--text-muted)"; }}
>
{copied
? <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12" /></svg>
: <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /></svg>}
</button>
<button
onClick={() => setExpanded((v) => !v)}
aria-label={expanded ? t("i18n.collapse") : t("i18n.expand")}
style={{ width: 32, display: "grid", placeItems: "center", border: "none", borderLeft: "1px solid var(--border)", background: "none", color: "var(--text-dim)", cursor: "pointer", flexShrink: 0 }}
>
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0, transform: expanded ? "rotate(180deg)" : "none", transition: "transform 0.15s" }}>
<polyline points="2 3.5 5 6.5 8 3.5" />
</svg>
</button>
Expand All @@ -1083,6 +1115,7 @@ function ToolCallBlock({ block, result, duration, onOpenSession }: { block: Tool
fontSize: "calc(12px + var(--chat-font-size-offset, 0px))",
lineHeight: 1.5,
overflow: "auto",
maxHeight: 300,
background: "var(--bg-subtle)",
borderTop: isError ? "1px solid rgba(248,113,113,0.25)" : "1px solid rgba(34,197,94,0.2)",
whiteSpace: "pre-wrap",
Expand Down Expand Up @@ -1402,7 +1435,7 @@ function PairedResult({ text, images, isEmpty, isError }: {
fontSize: "calc(12px + var(--chat-font-size-offset, 0px))",
lineHeight: 1.5,
overflow: "auto",
maxHeight: 400,
maxHeight: 300,
background: "var(--bg)",
whiteSpace: "pre-wrap",
wordBreak: "break-all",
Expand Down Expand Up @@ -1488,11 +1521,12 @@ function CompactionFileMetadata({ readFiles, modifiedFiles }: { readFiles: strin
}

function CompactionFileList({ title, files }: { title: string; files: string[] }) {
const uniqueFiles = Array.from(new Set(files));
return (
<div className="compaction-file-section">
<div className="compaction-file-title">{title}</div>
<ul className="compaction-file-list">
{files.map((file) => (
{uniqueFiles.map((file) => (
<li key={file}>{file}</li>
))}
</ul>
Expand Down
46 changes: 39 additions & 7 deletions hooks/useAgentSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export type AgentPhase =
| { kind: "waiting_model" }
| { kind: "running_command" }
| { kind: "running_tools"; tools: { id: string; name: string; progress?: string }[] }
| { kind: "compacting"; auto: boolean }
| null;

export interface CompactResultInfo {
Expand Down Expand Up @@ -540,21 +541,45 @@ export function useAgentSession(opts: UseAgentSessionOptions) {
if (sessionIdRef.current !== sid || options?.signal?.aborted || !sessionHookMountedRef.current) return;
setHistoryCursor(d.context.oldestEntryId);
setHasEarlierMessages(d.context.hasMore);
// Deduplicate: the active branch may have changed while the pagination
// request was in flight (e.g. loadSession ran from agent_end), so the
// returned ancestors may already exist in the current state. Compute
// the indices of entries whose entryId is not already present.
const dedupeIndices = (existing: Set<string>): number[] => {
const seen = new Set<string>();
const indices: number[] = [];
for (let i = 0; i < d.context.entryIds.length; i++) {
const id = d.context.entryIds[i];
if (id && !existing.has(id) && !seen.has(id)) {
indices.push(i);
seen.add(id);
}
}
return indices;
};

setData((prev) => {
if (!prev || prev.sessionId !== sid) return prev;
const context = before ? {
if (!before) return { ...prev, context: d.context };
const indices = dedupeIndices(new Set(prev.context.entryIds));
return { ...prev, context: {
...prev.context,
messages: [...d.context.messages, ...prev.context.messages],
entryIds: [...d.context.entryIds, ...prev.context.entryIds],
messages: [...indices.map(i => d.context.messages[i]), ...prev.context.messages],
entryIds: [...indices.map(i => d.context.entryIds[i]), ...prev.context.entryIds],
oldestEntryId: d.context.oldestEntryId,
hasMore: d.context.hasMore,
} : d.context;
return { ...prev, context };
}};
});
if (before) {
// Older page: prepend so scroll position stays anchored.
setMessages((prev) => [...d.context.messages, ...prev]);
setEntryIds((prev) => [...d.context.entryIds, ...prev]);
const indices = dedupeIndices(new Set(entryIds));
const newMessages = indices.map(i => d.context.messages[i]);
const newEntryIds = indices.map(i => d.context.entryIds[i]);
setMessages((prev) => [...newMessages, ...prev]);
setEntryIds((prev) => {
const existingPrev = new Set(prev);
return [...newEntryIds.filter(id => !existingPrev.has(id)), ...prev];
});
} else {
setMessages(d.context.messages);
setEntryIds(d.context.entryIds ?? []);
Expand Down Expand Up @@ -1259,14 +1284,21 @@ export function useAgentSession(opts: UseAgentSessionOptions) {
setRetryInfo(null);
break;
case "auto_compaction_start":
setIsCompacting(true);
setCompactError(null);
setCompactResult(null);
setAgentPhase({ kind: "compacting", auto: true });
break;
case "compaction_start":
setIsCompacting(true);
setCompactError(null);
setCompactResult(null);
setAgentPhase({ kind: "compacting", auto: false });
break;
case "auto_compaction_end":
case "compaction_end":
setIsCompacting(false);
setAgentPhase(null);
if (event.errorMessage) {
setCompactError(event.errorMessage as string);
setCompactResult(null);
Expand Down
7 changes: 6 additions & 1 deletion lib/agent-event-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,12 @@ export class AgentEventConnection {

close(): void {
this.stopRetrying();
if (this.current) this.discard(this.current, new AgentEventConnectionError("closed"));
if (this.current) {
const conn = this.current;
this.current = null;
conn.attempt.succeed();
conn.source.close();
}
}

maintain(sessionId: string): void {
Expand Down
4 changes: 4 additions & 0 deletions lib/i18n/messages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,10 @@ export const enLocale: LocalePlugin = {
"chat.stopCompaction": "Stop compaction",
"chat.compactContext": "Compact context",
"chat.compacting": "Compacting…",
"chat.autoCompacting": "Auto-compacting…",
"chat.undo": "Undo",
"chat.redo": "Redo",
"chat.checkpoint": "Checkpoint",
"chat.compact": "Compact",
"chat.stopAgent": "Stop agent",
"chat.stop": "Stop",
Expand Down
4 changes: 4 additions & 0 deletions lib/i18n/messages/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,10 @@ export const zhCNLocale: LocalePlugin = {
"chat.stopCompaction": "停止压缩",
"chat.compactContext": "压缩上下文",
"chat.compacting": "正在压缩…",
"chat.autoCompacting": "正在自动压缩…",
"chat.undo": "撤销",
"chat.redo": "重做",
"chat.checkpoint": "检查点",
"chat.compact": "压缩",
"chat.stopAgent": "停止 Agent",
"chat.stop": "停止",
Expand Down
3 changes: 3 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ try {

const nextConfig: NextConfig = {
outputFileTracingRoot: configDir,
outputFileTracingExcludes: {
"*": ["**/AppData/Local/Microsoft/Windows/UPPS/**"],
},
serverExternalPackages: [
"node-pty",
"undici",
Expand Down
Loading