@baique/pi-web-sky 0.1.59 → 0.1.61
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 +7 -7
- package/.next/build-manifest.json +3 -3
- package/.next/react-loadable-manifest.json +1 -1
- package/.next/required-server-files.js +1 -1
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.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_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +3 -3
- package/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/agent/running/route.js +1 -1
- package/.next/server/app/api/app-update/route.js +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +5 -5
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/index.segments/_full.segment.rsc +5 -5
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/page.js +8 -8
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +7 -7
- package/.next/server/chunks/1937.js +1 -1
- package/.next/server/chunks/402.js +8 -8
- package/.next/server/chunks/542.js +1 -1
- package/.next/server/chunks/6429.js +1 -1
- package/.next/server/chunks/6525.js +4 -4
- package/.next/server/chunks/6583.js +1 -1
- 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/static/chunks/{8088.39866dc5f1d15868.js → 8088.a8a689f302a4f56d.js} +1 -1
- package/.next/static/chunks/app/{layout-15b8d7ddfb49a19d.js → layout-58dd6111497e161d.js} +1 -1
- package/.next/static/chunks/app/page-9bbf25bd34fa91f1.js +256 -0
- package/.next/static/chunks/{webpack-1ce35e1249cfd931.js → webpack-4b8e9412e72f86c0.js} +1 -1
- package/.next/static/css/21bd3ff21f2866ac.css +1 -0
- package/.next/trace +7 -8
- package/.next/trace-build +1 -1
- package/lib/board-types.ts +6 -0
- package/lib/rpc-manager.title.test.mjs +128 -0
- package/lib/rpc-manager.ts +25 -0
- package/lib/session-index-scanner.test.mjs +25 -0
- package/lib/session-index-scanner.ts +9 -0
- package/lib/session-list-signal.test.mjs +50 -0
- package/lib/session-list-signal.ts +32 -0
- package/lib/session-reader.ts +5 -6
- package/lib/task-store.ts +5 -3
- package/lib/transcript.test.mjs +186 -0
- package/lib/transcript.ts +304 -0
- package/lib/turn-merge.ts +1 -1
- package/package.json +1 -1
- package/.next/static/chunks/app/page-06102d7540ccaf75.js +0 -256
- package/.next/static/css/c7d127e07b1a786c.css +0 -1
- /package/.next/static/{qD0gvlcxBrjwm2vdTuA1h → ImiqQqbpF4KxeBm1KeB5N}/_buildManifest.js +0 -0
- /package/.next/static/{qD0gvlcxBrjwm2vdTuA1h → ImiqQqbpF4KxeBm1KeB5N}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import type { AgentMessage } from "@/lib/types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 会话消息列表的单一 owner(纯函数 reducer)。
|
|
5
|
+
*
|
|
6
|
+
* 为什么要它:消息列表原先由 `messages` / `entryIds` / `parentIds` **三个并行数组**维护,
|
|
7
|
+
* 长度还刻意不一致(发送时先乐观追加 messages、entryIds 不动),于是"对齐"变成每个写入者的义务,
|
|
8
|
+
* 而"刚发出的消息是哪一个"只能靠「是不是最后一条 + 文本相等」去猜 —— 猜错就重复,被整表替换就消失,
|
|
9
|
+
* 身份变化就卸载重挂(闪烁)。上游 issue #23 / #24 就是同一类症状,当时的修法(乐观 key + 相邻判定)
|
|
10
|
+
* 是启发式补丁,我们 fork 时继承的就是那个形态。
|
|
11
|
+
*
|
|
12
|
+
* 这里的规矩只有两条:
|
|
13
|
+
* 1. 每条消息在列表里出现且仅出现一次;
|
|
14
|
+
* 2. 一条条目的身份(`id`)从创建到销毁不变 —— 渲染 key、滚动锚点、认领回声都用它,
|
|
15
|
+
* 服务端 entry id 只是落盘后挂上去的元数据。
|
|
16
|
+
*
|
|
17
|
+
* 所有"谁是谁"的判断都换成确定性依据:
|
|
18
|
+
* - 已有 serverEntryId 的条目 ↔ 服务端窗口:按 entryId 配对;
|
|
19
|
+
* - 无 serverEntryId 的条目(SSE 直接追加的、或回声后的 pending):按 role + timestamp 配对;
|
|
20
|
+
* - pending 用户条目 ↔ 服务端用户条目:按提交顺序 FIFO,且服务端 timestamp 不早于提交时刻。
|
|
21
|
+
* 不出现任何"文本相等"的猜测。
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export interface TranscriptItem {
|
|
25
|
+
/** 客户端 id:从创建到销毁不变(渲染 key 用它)。 */
|
|
26
|
+
id: string;
|
|
27
|
+
/** 服务端落盘后的 entry id;未落盘(pending / 仅经 SSE 到达)时为 null。 */
|
|
28
|
+
serverEntryId: string | null;
|
|
29
|
+
parentId: string | null;
|
|
30
|
+
message: AgentMessage;
|
|
31
|
+
/** pending = 已提交、尚未在服务端列表里确认;confirmed = 已确认(或不经本地回显直接到达)。 */
|
|
32
|
+
state: "pending" | "confirmed";
|
|
33
|
+
/** 提交时的运行序号(把回声认领回正确的 pending 条目)。 */
|
|
34
|
+
runId: number | null;
|
|
35
|
+
/** 提交时刻(ms)。合并时用它判断"服务端这条是不是我提交的那条"。 */
|
|
36
|
+
submittedAt: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 服务端窗口里的一条 entry(loadSession / loadContext 的返回形态)。 */
|
|
40
|
+
export interface ServerEntry {
|
|
41
|
+
entryId: string;
|
|
42
|
+
parentId: string | null;
|
|
43
|
+
message: AgentMessage;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface TranscriptState {
|
|
47
|
+
items: TranscriptItem[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type TranscriptAction =
|
|
51
|
+
/** 用户提交一条消息(本地立即上屏,等回声确认)。 */
|
|
52
|
+
| { type: "submit"; id: string; message: AgentMessage; runId: number; submittedAt: number }
|
|
53
|
+
/** SSE message_end 到达的非用户消息(assistant / toolResult / system …),服务端已经产出。 */
|
|
54
|
+
| { type: "server"; message: AgentMessage }
|
|
55
|
+
/** SSE message_end 到达的用户消息:认领对应的 pending 条目并**原位升级**;没有 pending 才追加。 */
|
|
56
|
+
| { type: "echo"; runId: number; message: AgentMessage }
|
|
57
|
+
/** 服务端尾部窗口(loadSession):按身份合并,pending 原位保留。 */
|
|
58
|
+
| { type: "mergeTail"; entries: ServerEntry[] }
|
|
59
|
+
/** 更早一页历史(loadContext before):前置插入并按 entryId 去重。 */
|
|
60
|
+
| { type: "prependOlder"; entries: ServerEntry[] }
|
|
61
|
+
/** 提交被拒:按 id 移除该 pending。 */
|
|
62
|
+
| { type: "fail"; id: string }
|
|
63
|
+
/** 切换分支 / 会话:显式清空(唯一允许清空列表的动作)。 */
|
|
64
|
+
| { type: "reset" };
|
|
65
|
+
|
|
66
|
+
export function createTranscriptState(): TranscriptState {
|
|
67
|
+
return { items: [] };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function transcriptMessages(state: TranscriptState): AgentMessage[] {
|
|
71
|
+
return state.items.map((item) => item.message);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 与服务端 entry 一一对应;pending 条目为 null。 */
|
|
75
|
+
export function transcriptEntryIds(state: TranscriptState): (string | null)[] {
|
|
76
|
+
return state.items.map((item) => item.serverEntryId);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function transcriptParentIds(state: TranscriptState): (string | null)[] {
|
|
80
|
+
return state.items.map((item) => item.parentId);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 渲染用:条目 id 顺序(ChatWindow 的 key / 滚动锚点)。 */
|
|
84
|
+
export function transcriptItemIds(state: TranscriptState): string[] {
|
|
85
|
+
return state.items.map((item) => item.id);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function timestampOf(message: AgentMessage): number | null {
|
|
89
|
+
const value = (message as { timestamp?: unknown }).timestamp;
|
|
90
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 无 entryId 的条目与服务端 entry 的确定性配对依据:角色 + 时间戳。 */
|
|
94
|
+
function sameRoleAndTimestamp(a: AgentMessage, b: AgentMessage): boolean {
|
|
95
|
+
if (a.role !== b.role) return false;
|
|
96
|
+
const at = timestampOf(a);
|
|
97
|
+
const bt = timestampOf(b);
|
|
98
|
+
return at !== null && bt !== null && at === bt;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function confirmedFromEntry(entry: ServerEntry, id: string): TranscriptItem {
|
|
102
|
+
return {
|
|
103
|
+
id,
|
|
104
|
+
serverEntryId: entry.entryId,
|
|
105
|
+
parentId: entry.parentId,
|
|
106
|
+
message: entry.message,
|
|
107
|
+
state: "confirmed",
|
|
108
|
+
runId: null,
|
|
109
|
+
submittedAt: 0,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 尾部窗口合并。
|
|
115
|
+
*
|
|
116
|
+
* 语义与旧的 `loadSession` 一致(尾部窗口 + 保留已加载的更早历史),但:
|
|
117
|
+
* - 命中已有条目的位置**沿用原 id**(不重挂、不闪);
|
|
118
|
+
* - 本地 pending 条目服务端还没落盘时**原位保留**(不消失);
|
|
119
|
+
* - 空窗口不动列表(避免瞬态空响应把界面刷白)。
|
|
120
|
+
*/
|
|
121
|
+
function mergeTail(state: TranscriptState, entries: ServerEntry[]): TranscriptState {
|
|
122
|
+
if (entries.length === 0) return state;
|
|
123
|
+
|
|
124
|
+
const items = state.items;
|
|
125
|
+
const claimedItem = new Set<number>();
|
|
126
|
+
const matchedItemByServerIdx = new Map<number, number>();
|
|
127
|
+
|
|
128
|
+
// ① 已有 entryId 的条目:按 entryId 配对
|
|
129
|
+
const itemIdxByEntryId = new Map<string, number>();
|
|
130
|
+
items.forEach((item, idx) => {
|
|
131
|
+
if (item.serverEntryId) itemIdxByEntryId.set(item.serverEntryId, idx);
|
|
132
|
+
});
|
|
133
|
+
entries.forEach((entry, serverIdx) => {
|
|
134
|
+
const itemIdx = itemIdxByEntryId.get(entry.entryId);
|
|
135
|
+
if (itemIdx === undefined || claimedItem.has(itemIdx)) return;
|
|
136
|
+
claimedItem.add(itemIdx);
|
|
137
|
+
matchedItemByServerIdx.set(serverIdx, itemIdx);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ② 无 entryId 的条目(SSE 直接追加的 / 回声后的 pending):按角色 + 时间戳配对
|
|
141
|
+
entries.forEach((entry, serverIdx) => {
|
|
142
|
+
if (matchedItemByServerIdx.has(serverIdx)) return;
|
|
143
|
+
const itemIdx = items.findIndex((item, idx) => (
|
|
144
|
+
!claimedItem.has(idx) && !item.serverEntryId && sameRoleAndTimestamp(item.message, entry.message)
|
|
145
|
+
));
|
|
146
|
+
if (itemIdx === -1) return;
|
|
147
|
+
claimedItem.add(itemIdx);
|
|
148
|
+
matchedItemByServerIdx.set(serverIdx, itemIdx);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ③ pending 用户条目 ↔ 未认领的服务端用户条目:提交顺序 FIFO,服务端时间不早于提交时刻
|
|
152
|
+
const pendingIdxList = items
|
|
153
|
+
.map((item, idx) => ({ item, idx }))
|
|
154
|
+
.filter(({ item, idx }) => item.state === "pending" && item.message.role === "user" && !claimedItem.has(idx))
|
|
155
|
+
.sort((a, b) => a.item.submittedAt - b.item.submittedAt)
|
|
156
|
+
.map(({ idx }) => idx);
|
|
157
|
+
entries.forEach((entry, serverIdx) => {
|
|
158
|
+
if (matchedItemByServerIdx.has(serverIdx) || entry.message.role !== "user") return;
|
|
159
|
+
const serverTimestamp = timestampOf(entry.message);
|
|
160
|
+
const itemIdx = pendingIdxList.find((idx) => (
|
|
161
|
+
!claimedItem.has(idx)
|
|
162
|
+
&& (serverTimestamp === null || serverTimestamp >= items[idx].submittedAt)
|
|
163
|
+
));
|
|
164
|
+
if (itemIdx === undefined) return;
|
|
165
|
+
claimedItem.add(itemIdx);
|
|
166
|
+
matchedItemByServerIdx.set(serverIdx, itemIdx);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ④ 组装:锚点之前的本地条目 = 已加载的更早历史,原样保留在前。
|
|
170
|
+
// 没有锚点时不能把本地条目当"更早历史"——它们是还没落盘的 pending,应该接在窗口之后。
|
|
171
|
+
const hasAnchor = matchedItemByServerIdx.size > 0;
|
|
172
|
+
const anchorItemIdx = hasAnchor
|
|
173
|
+
? Math.min(...matchedItemByServerIdx.values())
|
|
174
|
+
: Number.POSITIVE_INFINITY;
|
|
175
|
+
|
|
176
|
+
const out: TranscriptItem[] = [];
|
|
177
|
+
if (hasAnchor) {
|
|
178
|
+
items.forEach((item, idx) => {
|
|
179
|
+
if (idx < anchorItemIdx && !claimedItem.has(idx)) out.push(item);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const consumedItemIdx = new Set<number>();
|
|
184
|
+
for (let serverIdx = 0; serverIdx < entries.length; serverIdx++) {
|
|
185
|
+
const entry = entries[serverIdx];
|
|
186
|
+
const itemIdx = matchedItemByServerIdx.get(serverIdx);
|
|
187
|
+
if (itemIdx === undefined) {
|
|
188
|
+
out.push(confirmedFromEntry(entry, `srv:${entry.entryId}`));
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const item = items[itemIdx];
|
|
192
|
+
consumedItemIdx.add(itemIdx);
|
|
193
|
+
// 原位升级:id 不变,只把服务端信息补上
|
|
194
|
+
out.push({
|
|
195
|
+
...item,
|
|
196
|
+
serverEntryId: entry.entryId,
|
|
197
|
+
parentId: entry.parentId,
|
|
198
|
+
message: entry.message,
|
|
199
|
+
state: "confirmed",
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ⑤ 尾巴:只接回**还没落盘**的本地条目(pending / 仅经 SSE 到达)。
|
|
204
|
+
// 有 serverEntryId 却不在窗口里的,说明它们已不在当前 leaf 的分支上(回退 / 「编辑从此处」/ 切分支)——
|
|
205
|
+
// 服务端窗口是当前分支的权威尾部,这些必须丢弃;否则回退后旧消息会继续挂在消息流里。
|
|
206
|
+
items.forEach((item, idx) => {
|
|
207
|
+
if (consumedItemIdx.has(idx) || claimedItem.has(idx)) return;
|
|
208
|
+
if (item.serverEntryId) return;
|
|
209
|
+
if (hasAnchor && idx < anchorItemIdx) return; // 已在 ④ 前置保留
|
|
210
|
+
out.push(item);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return { items: out };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function prependOlder(state: TranscriptState, entries: ServerEntry[]): TranscriptState {
|
|
217
|
+
if (entries.length === 0) return state;
|
|
218
|
+
const known = new Set(state.items.map((item) => item.serverEntryId).filter(Boolean) as string[]);
|
|
219
|
+
const fresh = entries.filter((entry) => !known.has(entry.entryId));
|
|
220
|
+
if (fresh.length === 0) return state;
|
|
221
|
+
return {
|
|
222
|
+
items: [
|
|
223
|
+
...fresh.map((entry) => confirmedFromEntry(entry, `srv:${entry.entryId}`)),
|
|
224
|
+
...state.items,
|
|
225
|
+
],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function transcriptReducer(state: TranscriptState, action: TranscriptAction): TranscriptState {
|
|
230
|
+
switch (action.type) {
|
|
231
|
+
case "submit":
|
|
232
|
+
return {
|
|
233
|
+
items: [...state.items, {
|
|
234
|
+
id: action.id,
|
|
235
|
+
serverEntryId: null,
|
|
236
|
+
parentId: null,
|
|
237
|
+
message: action.message,
|
|
238
|
+
state: "pending",
|
|
239
|
+
runId: action.runId,
|
|
240
|
+
submittedAt: action.submittedAt,
|
|
241
|
+
}],
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
case "server":
|
|
245
|
+
return {
|
|
246
|
+
items: [...state.items, {
|
|
247
|
+
id: `srv-live:${state.items.length}:${action.message.role}`,
|
|
248
|
+
serverEntryId: null,
|
|
249
|
+
parentId: null,
|
|
250
|
+
message: action.message,
|
|
251
|
+
state: "confirmed",
|
|
252
|
+
runId: null,
|
|
253
|
+
submittedAt: 0,
|
|
254
|
+
}],
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
case "echo": {
|
|
258
|
+
// 认领:优先同一 run 的 pending,其次最早的 pending(FIFO)
|
|
259
|
+
const pending = state.items
|
|
260
|
+
.map((item, idx) => ({ item, idx }))
|
|
261
|
+
.filter(({ item }) => item.state === "pending" && item.message.role === "user")
|
|
262
|
+
.sort((a, b) => a.item.submittedAt - b.item.submittedAt);
|
|
263
|
+
const target = pending.find(({ item }) => item.runId === action.runId) ?? pending[0];
|
|
264
|
+
if (target) {
|
|
265
|
+
// 原位升级:id 不变 → 渲染不重挂;服务端内容为准
|
|
266
|
+
const items = [...state.items];
|
|
267
|
+
items[target.idx] = {
|
|
268
|
+
...target.item,
|
|
269
|
+
message: action.message,
|
|
270
|
+
state: "confirmed",
|
|
271
|
+
runId: action.runId,
|
|
272
|
+
};
|
|
273
|
+
return { items };
|
|
274
|
+
}
|
|
275
|
+
// 没有本地回显(别的客户端/扩展发的)→ 作为新条目追加
|
|
276
|
+
return {
|
|
277
|
+
items: [...state.items, {
|
|
278
|
+
id: `echo:${action.runId}:${state.items.length}`,
|
|
279
|
+
serverEntryId: null,
|
|
280
|
+
parentId: null,
|
|
281
|
+
message: action.message,
|
|
282
|
+
state: "confirmed",
|
|
283
|
+
runId: action.runId,
|
|
284
|
+
submittedAt: 0,
|
|
285
|
+
}],
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
case "mergeTail":
|
|
290
|
+
return mergeTail(state, action.entries);
|
|
291
|
+
|
|
292
|
+
case "prependOlder":
|
|
293
|
+
return prependOlder(state, action.entries);
|
|
294
|
+
|
|
295
|
+
case "fail":
|
|
296
|
+
return { items: state.items.filter((item) => item.id !== action.id) };
|
|
297
|
+
|
|
298
|
+
case "reset":
|
|
299
|
+
return { items: [] };
|
|
300
|
+
|
|
301
|
+
default:
|
|
302
|
+
return state;
|
|
303
|
+
}
|
|
304
|
+
}
|
package/lib/turn-merge.ts
CHANGED
|
@@ -71,7 +71,7 @@ export function getAssistantAnswerMarkdown(message: AgentMessage | Partial<Agent
|
|
|
71
71
|
*/
|
|
72
72
|
export function buildDomTurns(
|
|
73
73
|
allMessages: (AgentMessage | Partial<AgentMessage>)[],
|
|
74
|
-
entryIds: string[],
|
|
74
|
+
entryIds: (string | null)[],
|
|
75
75
|
): DomTurn[] {
|
|
76
76
|
const turns: DomTurn[] = [];
|
|
77
77
|
let current: DomTurn | null = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@baique/pi-web-sky",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.61",
|
|
4
4
|
"description": "Web UI for the pi coding agent — Sky skin edition (fork of @agegr/pi-web / earendil-labs pi-web, MIT)",
|
|
5
5
|
"homepage": "https://github.com/baique/pi-web-sky#readme",
|
|
6
6
|
"repository": {
|