Skip to content
Merged
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
61 changes: 61 additions & 0 deletions src/extensions/paimon/compact_abort_patch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Compaction Abort Patch — 截获 AgentSession 实例以调用 abortCompaction()
*
* ## 背景
* pi SDK 的 Extension API 只暴露了通用的 `ctx.abort()`(中止 agent streaming),
* 而上下文压缩使用独立的 `session.abortCompaction()` 方法取消。
* Extension context 未暴露该方法,导致 paimon 无法在 compact 期间提供终止能力。
*
* ## 原理
* Monkey-patch `AgentSession.prototype.compact`:
* 1. compact 被调用时保存 `this`(AgentSession 实例)引用
* 2. compact 结束后清除引用
* 3. 外部调用 `abortCompaction()` 时,转发到保存的 session 实例
*
* ## 兼容性
* 依赖 `AgentSession` 从 `@earendil-works/pi-coding-agent` 公开导出,
* 以及 `compact` / `abortCompaction` 方法签名不变。
* 如 patch 失败,compact 取消功能降级为不可用(不影响其他功能)。
*/

import { AgentSession } from "@earendil-works/pi-coding-agent";

// ─── 内部状态 ─────────────────────────────────────────────────

let _session: any = null;
let _patched = false;

// ─── 对外 API ─────────────────────────────────────────────────

/** 安装 patch。应在 extension 加载时调用一次。返回是否成功。 */
export function install(): boolean {
if (_patched) return true;
try {
const proto = AgentSession.prototype as any;
const origCompact = proto.compact;
if (typeof origCompact !== "function") return false;

proto.compact = async function (...args: any[]) {
_session = this;
try {
return await origCompact.apply(this, args);
} finally {
_session = null;
}
};

_patched = true;
return true;
} catch {
return false;
}
}

/** 尝试终止正在进行的 compaction。返回是否成功调用。 */
export function abortCompaction(): boolean {
if (_session && typeof _session.abortCompaction === "function") {
_session.abortCompaction();
return true;
}
return false;
}
17 changes: 13 additions & 4 deletions src/extensions/paimon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@ import type {
import { HubClient } from "./client";
import { serializeEvent, FORWARDED_EVENTS } from "./serializer";
import * as sessionControlPatch from "./session_control_patch";
import * as compactionAbortPatch from "./compact_abort_patch";
import { querySessionList, emptySessionListMessage } from "./session_list";

// Hub spawn 实例时注入的一次性 token(仅页面创建的实例有)。
// 注册时回传给 Hub,用于将 spawn 请求与注册成功的实例对应起来。
const SPAWN_TOKEN = process.env.PAIMON_SPAWN_TOKEN;

// 跟踪 compaction 状态(兜底用)
let compacting = false;

export default function (pi: ExtensionAPI) {
// pi extension 连接本机 Edge(不再直连 Hub)
const port = parseInt(
Expand All @@ -36,11 +40,11 @@ export default function (pi: ExtensionAPI) {
let registered = false;
// 保存最新的 ctx 引用,用于响应 get_history
let currentCtx: ExtensionContext | null = null;
// 跟踪 compaction 状态(兜底用)
let compacting = false;

// 安装 session 控制 patch(截获 newSession/switchSession 函数引用)
sessionControlPatch.install();
// 安装 compaction abort patch(截获 session 实例以支持取消压缩)
compactionAbortPatch.install();

const currentHostname = hostname();

Expand Down Expand Up @@ -316,8 +320,13 @@ function handleHubMessage(
pi.sendUserMessage(msg.payload.message, { deliverAs: "steer" });
break;
case "abort": {
const ctx = getCurrentCtx();
ctx?.abort();
if (compacting) {
// compacting 状态下取消压缩
compactionAbortPatch.abortCompaction();
} else {
const ctx = getCurrentCtx();
ctx?.abort();
}
break;
}
case "set_model": {
Expand Down
4 changes: 2 additions & 2 deletions src/web/src/components/InstanceView/Composer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe("ComposerStatusIndicator", () => {
expect(getComposerButtonMode("streaming")).toBe("stop");
});

test("shows send button during compacting", () => {
expect(getComposerButtonMode("compacting")).toBe("send");
test("shows stop button during compacting to allow abort", () => {
expect(getComposerButtonMode("compacting")).toBe("stop");
});
});
6 changes: 4 additions & 2 deletions src/web/src/components/InstanceView/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// InstanceView 纯计算工具函数

import type { InstanceStatus } from "../../../../protocol/types";
import { isStreaming as isStatusStreaming } from "../../utils/status";

const BOTTOM_FOLLOW_EPSILON = 0.5;

Expand Down Expand Up @@ -40,7 +39,10 @@ export function calculatePrependScrollTop({
}

export function getComposerButtonMode(instanceStatus?: InstanceStatus) {
return isStatusStreaming(instanceStatus) ? "stop" : "send";
// streaming 和 compacting 状态都显示 stop 按钮,允许用户终止
return instanceStatus === "streaming" || instanceStatus === "compacting"
? "stop"
: "send";
}

export function getSafeScrollTop(rawScrollTop: number) {
Expand Down