multiarena 0.1.3 → 0.1.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/CHANGELOG.md +21 -0
- package/dist/core/deliberation.d.ts +3 -2
- package/dist/core/deliberation.js +129 -10
- package/dist/core/session.d.ts +2 -0
- package/dist/core/session.js +6 -0
- package/dist/ui/app.js +75 -20
- package/dist/ui/components/DeliberationView.d.ts +1 -0
- package/dist/ui/components/DeliberationView.js +13 -7
- package/dist/ui/components/OutputArea.d.ts +1 -0
- package/dist/ui/components/OutputArea.js +3 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.1.4
|
|
4
|
+
|
|
5
|
+
### 关键升级
|
|
6
|
+
|
|
7
|
+
- **团队共享上下文** — 团队模式新增 `teamMessages` 共享消息线程,所有模型读写同一份对话历史。审议引擎从共享历史读取上下文而非零基础构建,每轮产出自动 push 回共享数组
|
|
8
|
+
- **私有思考阶段(Think Phase)** — 审议每轮之前模型先进行私有分析:审视当前文档优劣、识别问题、规划修改方案,再基于分析结果动手修改。思考内容不进入共享上下文(其他模型不可见),防止锚定偏差,同时拦截幻觉
|
|
9
|
+
- **"继续修改"Bug 修复** — 审议完成后在团队概览输入新要求,新一轮审议通过共享上下文自然看到上一轮全部产出和用户新指令,不再出现"缺失必要信息"错误
|
|
10
|
+
- **上下文管理文档** — 新增 `docs/multiarena-context-and-prompts.md`,详细记录模型上下文管理架构和系统提示词设计
|
|
11
|
+
|
|
12
|
+
### 关键 Bug 修复
|
|
13
|
+
|
|
14
|
+
- 删除 `[团队审议结果]` 手动注入逻辑 — 共享上下文已包含所有审议产出,无需事后打补丁
|
|
15
|
+
- 团队私聊(Tab 到模型)使用 `teamMessages` 作为上下文,与审议共享同一消息线程
|
|
16
|
+
|
|
17
|
+
### 测试
|
|
18
|
+
|
|
19
|
+
- 新增"继续修改"端到端测试(第一轮审议产出 → 用户追加消息 → 第二轮审议能访问完整上文)
|
|
20
|
+
- 全部 30 个测试文件、312 个测试用例通过
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
3
24
|
## v0.1.3
|
|
4
25
|
|
|
5
26
|
### 关键升级
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ModelConfig } from "../config/types.js";
|
|
2
|
+
import type { Message } from "../provider/types.js";
|
|
2
3
|
export type RoundRole = "draft" | "revise" | "polish" | "review";
|
|
3
4
|
export interface DeliberationRoundConfig {
|
|
4
5
|
modelName: string;
|
|
@@ -6,7 +7,7 @@ export interface DeliberationRoundConfig {
|
|
|
6
7
|
config: ModelConfig;
|
|
7
8
|
}
|
|
8
9
|
export interface DeliberationProgress {
|
|
9
|
-
type: "round_start" | "text" | "round_end" | "done" | "error";
|
|
10
|
+
type: "think_start" | "think_text" | "think_end" | "round_start" | "text" | "round_end" | "done" | "error";
|
|
10
11
|
round: number;
|
|
11
12
|
totalRounds: number;
|
|
12
13
|
modelName?: string;
|
|
@@ -36,7 +37,7 @@ export interface DeliberationResult {
|
|
|
36
37
|
* round output for context. No central synthesizer — the document
|
|
37
38
|
* emerges through sequential refinement.
|
|
38
39
|
*/
|
|
39
|
-
export declare function runDeliberation(
|
|
40
|
+
export declare function runDeliberation(sharedMessages: Message[], roundConfigs: DeliberationRoundConfig[], constraint?: string, worktreePath?: string): AsyncGenerator<DeliberationProgress>;
|
|
40
41
|
/** Build round configs with mirror pattern: A→B→C→B→A. */
|
|
41
42
|
export declare function autoAssignRounds(modelNames: string[], models: Record<string, ModelConfig>): DeliberationRoundConfig[];
|
|
42
43
|
/** Human-readable label for a round role. */
|
|
@@ -98,6 +98,61 @@ ${previousDocument}
|
|
|
98
98
|
return "";
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Build a private "think" prompt for the given role.
|
|
103
|
+
* The model analyses the current state before acting — this output is NOT
|
|
104
|
+
* shared with other models, but is fed back into the same model's main
|
|
105
|
+
* round system prompt so its public output is informed by private reasoning.
|
|
106
|
+
*/
|
|
107
|
+
function buildThinkPrompt(role, task, isFirstRound, constraint, previousDocument) {
|
|
108
|
+
const docBlock = previousDocument
|
|
109
|
+
? `\n\n## 当前文档(请仔细分析)\n${previousDocument}`
|
|
110
|
+
: "";
|
|
111
|
+
const taskBlock = `\n\n## 用户任务/反馈\n${task}`;
|
|
112
|
+
const constraintBlock = constraint
|
|
113
|
+
? `\n\n## 约束文档\n${constraint}`
|
|
114
|
+
: "";
|
|
115
|
+
switch (role) {
|
|
116
|
+
case "draft":
|
|
117
|
+
return `你即将以**起草者**的身份撰写一份初稿。在此之前,请先进行私有分析:
|
|
118
|
+
${taskBlock}${constraintBlock}
|
|
119
|
+
|
|
120
|
+
请简短回答以下问题(用你自己的话,不要长篇大论):
|
|
121
|
+
1. 任务的核心目标是什么?需要覆盖哪些关键要点?
|
|
122
|
+
2. 文档应该是什么结构?(章节/段落规划)
|
|
123
|
+
3. 有什么需要特别注意的约束或陷阱?
|
|
124
|
+
4. 有什么地方信息不足,需要合理假设的?`;
|
|
125
|
+
case "revise":
|
|
126
|
+
return `你即将以**修订者**的身份修改一份文档。在此之前,请先进行私有分析:
|
|
127
|
+
${taskBlock}${constraintBlock}${docBlock}
|
|
128
|
+
|
|
129
|
+
请简短回答以下问题:
|
|
130
|
+
1. 这份文档的优点是什么?哪些部分写得不错?
|
|
131
|
+
2. 存在哪些问题?(偏离任务、遗漏要点、逻辑不严谨、表达不清、潜在的事实错误或幻觉)
|
|
132
|
+
3. 对照约束文档,哪些地方违反了约束?
|
|
133
|
+
4. 你计划做哪些具体修改?按优先级列出。${isFirstRound ? "\n注意:这是第一轮修订,你看到的是初稿。重点关注初稿是否忠实地回应了用户的任务。" : ""}`;
|
|
134
|
+
case "polish":
|
|
135
|
+
return `你即将以**润色者**的身份最终润色一份文档。在此之前,请先进行私有分析:
|
|
136
|
+
${taskBlock}${constraintBlock}${docBlock}
|
|
137
|
+
|
|
138
|
+
请简短回答以下问题:
|
|
139
|
+
1. 文档的整体语言质量如何?(流畅度、可读性、语气一致性)
|
|
140
|
+
2. 有哪些表达可以更优雅或更精准?
|
|
141
|
+
3. 对照约束文档,还有什么需要修正的?
|
|
142
|
+
4. 你独有的补充见解是什么?(如果有的话)`;
|
|
143
|
+
case "review":
|
|
144
|
+
return `你即将以**终审者**的身份做最终审查。在此之前,请先进行私有分析:
|
|
145
|
+
${taskBlock}${constraintBlock}${docBlock}
|
|
146
|
+
|
|
147
|
+
请简短回答以下问题:
|
|
148
|
+
1. 经过多轮修改后,文档是否偏离了用户的原始意图?
|
|
149
|
+
2. 逐条对照约束文档检查——还有违规项吗?
|
|
150
|
+
3. 有没有任何模型引入了事实错误或幻觉?
|
|
151
|
+
4. 最终交付前,还有什么必须清理或修复的?`;
|
|
152
|
+
default:
|
|
153
|
+
return "";
|
|
154
|
+
}
|
|
155
|
+
}
|
|
101
156
|
/**
|
|
102
157
|
* Run the R2D2 (Round-Robin Deliberative Drafting) pipeline.
|
|
103
158
|
*
|
|
@@ -107,14 +162,17 @@ ${previousDocument}
|
|
|
107
162
|
* round output for context. No central synthesizer — the document
|
|
108
163
|
* emerges through sequential refinement.
|
|
109
164
|
*/
|
|
110
|
-
export async function* runDeliberation(
|
|
165
|
+
export async function* runDeliberation(sharedMessages, roundConfigs, constraint, worktreePath) {
|
|
111
166
|
const documents = [];
|
|
112
167
|
const totalRounds = roundConfigs.length;
|
|
168
|
+
// Derive the task from the last user message in the shared context.
|
|
169
|
+
const lastUser = [...sharedMessages].reverse().find((m) => m.role === "user");
|
|
170
|
+
const task = lastUser?.content ?? "";
|
|
113
171
|
for (let i = 0; i < roundConfigs.length; i++) {
|
|
114
172
|
const rc = roundConfigs[i];
|
|
115
173
|
const previousDocument = i > 0 ? documents[i - 1] : undefined;
|
|
116
174
|
const draftAuthor = i > 0 ? roundConfigs[0].modelName : undefined;
|
|
117
|
-
const
|
|
175
|
+
const isFinalRound = i === roundConfigs.length - 1;
|
|
118
176
|
yield {
|
|
119
177
|
type: "round_start",
|
|
120
178
|
round: i + 1,
|
|
@@ -122,14 +180,74 @@ export async function* runDeliberation(task, roundConfigs, constraint, worktreeP
|
|
|
122
180
|
modelName: rc.modelName,
|
|
123
181
|
role: rc.role,
|
|
124
182
|
};
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
183
|
+
// ── Private Think Phase ──────────────────────────────────────
|
|
184
|
+
// The model analyses the current state privately before acting.
|
|
185
|
+
// This output is NOT shared with other models — it only informs
|
|
186
|
+
// this model's own main round via the system prompt.
|
|
187
|
+
let thinkOutput = "";
|
|
188
|
+
const thinkPrompt = buildThinkPrompt(rc.role, task, i === 0, constraint, previousDocument);
|
|
189
|
+
if (thinkPrompt) {
|
|
190
|
+
yield {
|
|
191
|
+
type: "think_start",
|
|
192
|
+
round: i + 1,
|
|
193
|
+
totalRounds,
|
|
194
|
+
modelName: rc.modelName,
|
|
195
|
+
role: rc.role,
|
|
196
|
+
};
|
|
197
|
+
try {
|
|
198
|
+
const thinkStream = runTurn({
|
|
199
|
+
modelName: rc.modelName,
|
|
200
|
+
config: rc.config,
|
|
201
|
+
messages: [{ role: "user", content: "请按上述要求进行分析。用简洁的语言回答,不要长篇大论。" }],
|
|
202
|
+
systemPrompt: thinkPrompt,
|
|
203
|
+
tools: [],
|
|
204
|
+
registry: new ToolRegistry(),
|
|
205
|
+
permission: new PermissionManager(),
|
|
206
|
+
worktreePath: worktreePath ?? process.cwd(),
|
|
207
|
+
});
|
|
208
|
+
for await (const event of thinkStream) {
|
|
209
|
+
if (event.type === "text") {
|
|
210
|
+
thinkOutput += event.content;
|
|
211
|
+
yield {
|
|
212
|
+
type: "think_text",
|
|
213
|
+
round: i + 1,
|
|
214
|
+
totalRounds,
|
|
215
|
+
modelName: rc.modelName,
|
|
216
|
+
role: rc.role,
|
|
217
|
+
content: event.content,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
else if (event.type === "error") {
|
|
221
|
+
// Think failure is non-fatal — proceed without think context
|
|
222
|
+
thinkOutput = "";
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
thinkOutput = "";
|
|
229
|
+
}
|
|
230
|
+
yield {
|
|
231
|
+
type: "think_end",
|
|
232
|
+
round: i + 1,
|
|
233
|
+
totalRounds,
|
|
234
|
+
modelName: rc.modelName,
|
|
235
|
+
role: rc.role,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
// ── Main Round System Prompt (informed by private think) ──────
|
|
239
|
+
const thinkBlock = thinkOutput
|
|
240
|
+
? `\n\n## 你的私有分析结果\n以下是你刚才对当前状态的分析。请基于这些洞察来完成你的任务:\n\n${thinkOutput}`
|
|
241
|
+
: "";
|
|
242
|
+
const systemPrompt = buildSystemPrompt(rc.role, task, isFinalRound, constraint, previousDocument, draftAuthor) + thinkBlock;
|
|
243
|
+
// Build messages from the shared context plus this round's role instruction.
|
|
244
|
+
const instruction = {
|
|
245
|
+
role: "user",
|
|
246
|
+
content: rc.role === "draft"
|
|
247
|
+
? `请起草以下文档:\n\n${task}`
|
|
248
|
+
: "请根据你的角色要求和上述文档内容,输出修改后的完整文档。不要输出任何前言或后记,直接输出文档内容。",
|
|
249
|
+
};
|
|
250
|
+
const messages = [...sharedMessages, instruction];
|
|
133
251
|
let buffer = "";
|
|
134
252
|
try {
|
|
135
253
|
const stream = runTurn({
|
|
@@ -180,6 +298,7 @@ export async function* runDeliberation(task, roundConfigs, constraint, worktreeP
|
|
|
180
298
|
return;
|
|
181
299
|
}
|
|
182
300
|
documents.push(buffer);
|
|
301
|
+
sharedMessages.push({ role: "assistant", content: buffer });
|
|
183
302
|
// Extract revision annotations for the process summary
|
|
184
303
|
const revisionMatches = buffer.match(/\[修订:\s*([^\]]+?)\]/g) ?? [];
|
|
185
304
|
const changeSamples = revisionMatches
|
package/dist/core/session.d.ts
CHANGED
|
@@ -16,12 +16,14 @@ export interface SessionSnapshot {
|
|
|
16
16
|
}>;
|
|
17
17
|
targetMode: TargetMode;
|
|
18
18
|
worktreeBase: string;
|
|
19
|
+
teamMessages?: Message[];
|
|
19
20
|
}
|
|
20
21
|
export declare class Session {
|
|
21
22
|
private state;
|
|
22
23
|
constructor(config: ArenaConfig, worktreeBase: string, snapshot?: SessionSnapshot);
|
|
23
24
|
get models(): ModelState[];
|
|
24
25
|
get targetMode(): TargetMode;
|
|
26
|
+
get teamMessages(): Message[];
|
|
25
27
|
/** Add a user message to the target model(s). Returns affected models. */
|
|
26
28
|
addUserMessage(content: string): ModelState[];
|
|
27
29
|
/** Append assistant response to a model's history */
|
package/dist/core/session.js
CHANGED
|
@@ -10,6 +10,7 @@ export class Session {
|
|
|
10
10
|
})),
|
|
11
11
|
targetMode: snapshot.targetMode,
|
|
12
12
|
worktreeBase: snapshot.worktreeBase,
|
|
13
|
+
teamMessages: snapshot.teamMessages ?? [],
|
|
13
14
|
};
|
|
14
15
|
}
|
|
15
16
|
else {
|
|
@@ -30,6 +31,7 @@ export class Session {
|
|
|
30
31
|
models,
|
|
31
32
|
targetMode: { type: "broadcast" },
|
|
32
33
|
worktreeBase,
|
|
34
|
+
teamMessages: [],
|
|
33
35
|
};
|
|
34
36
|
}
|
|
35
37
|
}
|
|
@@ -39,6 +41,9 @@ export class Session {
|
|
|
39
41
|
get targetMode() {
|
|
40
42
|
return this.state.targetMode;
|
|
41
43
|
}
|
|
44
|
+
get teamMessages() {
|
|
45
|
+
return this.state.teamMessages;
|
|
46
|
+
}
|
|
42
47
|
/** Add a user message to the target model(s). Returns affected models. */
|
|
43
48
|
addUserMessage(content) {
|
|
44
49
|
if (this.state.targetMode.type === "broadcast") {
|
|
@@ -136,6 +141,7 @@ export class Session {
|
|
|
136
141
|
})),
|
|
137
142
|
targetMode: this.state.targetMode,
|
|
138
143
|
worktreeBase: this.state.worktreeBase,
|
|
144
|
+
teamMessages: [...this.state.teamMessages],
|
|
139
145
|
};
|
|
140
146
|
}
|
|
141
147
|
findModel(name) {
|
package/dist/ui/app.js
CHANGED
|
@@ -62,6 +62,7 @@ export const App = ({ sessionId: initialSessionId }) => {
|
|
|
62
62
|
// ── Deliberation state ─────────────────────────────────────────
|
|
63
63
|
const [deliberationProgress, setDeliberationProgress] = useState(null);
|
|
64
64
|
const [deliberationDocument, setDeliberationDocument] = useState("");
|
|
65
|
+
const [deliberationThinkText, setDeliberationThinkText] = useState("");
|
|
65
66
|
const [deliberationRounds, setDeliberationRounds] = useState([]);
|
|
66
67
|
const [deliberationScrollOffset, setDeliberationScrollOffset] = useState(0);
|
|
67
68
|
const deliberatingRef = useRef(false);
|
|
@@ -377,7 +378,7 @@ export const App = ({ sessionId: initialSessionId }) => {
|
|
|
377
378
|
}
|
|
378
379
|
});
|
|
379
380
|
// ── Deliberation runner ───────────────────────────────────────
|
|
380
|
-
const runDeliberationPipeline = useCallback(async (
|
|
381
|
+
const runDeliberationPipeline = useCallback(async () => {
|
|
381
382
|
if (deliberatingRef.current)
|
|
382
383
|
return;
|
|
383
384
|
deliberatingRef.current = true;
|
|
@@ -434,12 +435,24 @@ export const App = ({ sessionId: initialSessionId }) => {
|
|
|
434
435
|
setDeliberationRounds([]);
|
|
435
436
|
setDeliberationScrollOffset(0);
|
|
436
437
|
let doc = "";
|
|
437
|
-
const stream = runDeliberation(
|
|
438
|
+
const stream = runDeliberation(session.teamMessages, roundConfigs, constraint);
|
|
438
439
|
for await (const event of stream) {
|
|
439
440
|
setDeliberationProgress(event);
|
|
440
|
-
if (event.type === "
|
|
441
|
+
if (event.type === "think_start") {
|
|
442
|
+
setDeliberationThinkText("");
|
|
443
|
+
setDeliberationDocument("");
|
|
444
|
+
}
|
|
445
|
+
else if (event.type === "think_text" && event.content) {
|
|
446
|
+
setDeliberationThinkText((prev) => prev + event.content);
|
|
447
|
+
}
|
|
448
|
+
else if (event.type === "think_end") {
|
|
449
|
+
// Think done — keep think text in state for UI reference,
|
|
450
|
+
// main round will populate deliberationDocument next.
|
|
451
|
+
}
|
|
452
|
+
else if (event.type === "round_start") {
|
|
441
453
|
doc = "";
|
|
442
454
|
setDeliberationDocument("");
|
|
455
|
+
setDeliberationThinkText("");
|
|
443
456
|
setDeliberationRounds((prev) => [
|
|
444
457
|
...prev,
|
|
445
458
|
{ round: event.round, modelName: event.modelName, role: event.role },
|
|
@@ -467,17 +480,6 @@ export const App = ({ sessionId: initialSessionId }) => {
|
|
|
467
480
|
return;
|
|
468
481
|
}
|
|
469
482
|
}
|
|
470
|
-
// Keep the final document visible; user presses Esc to dismiss
|
|
471
|
-
// Inject the final document into each model's message history
|
|
472
|
-
// so they can discuss it when the user switches to chat mode.
|
|
473
|
-
if (doc) {
|
|
474
|
-
const contextMsg = `[团队审议结果]\n\n以下是你与其他模型协作完成的最终文档。用户可以就此文档与你讨论。\n\n---\n${doc}\n---`;
|
|
475
|
-
for (const m of session.models) {
|
|
476
|
-
if (!m.muted) {
|
|
477
|
-
m.messages.push({ role: "user", content: contextMsg });
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
483
|
deliberatingRef.current = false;
|
|
482
484
|
}, [session.models, config]);
|
|
483
485
|
// ── Merge runner ──────────────────────────────────────────────
|
|
@@ -572,18 +574,71 @@ export const App = ({ sessionId: initialSessionId }) => {
|
|
|
572
574
|
const r = reduceSubmitInTeam(currentModeState(), isOverview());
|
|
573
575
|
if (r.action === "block")
|
|
574
576
|
return;
|
|
577
|
+
inputHistoryRef.current.push(trimmed);
|
|
578
|
+
historyIdxRef.current = -1;
|
|
579
|
+
setInput("");
|
|
580
|
+
// All team interactions share session.teamMessages as context.
|
|
581
|
+
session.teamMessages.push({ role: "user", content: trimmed });
|
|
575
582
|
if (r.action === "deliberate") {
|
|
576
|
-
inputHistoryRef.current.push(trimmed);
|
|
577
|
-
historyIdxRef.current = -1;
|
|
578
|
-
setInput("");
|
|
579
583
|
setDeliberationDocument("");
|
|
580
584
|
// Reset target to overview so OutputArea shows deliberation progress
|
|
581
585
|
session.setTarget({ type: "broadcast" });
|
|
582
586
|
setTargetVersion((v) => v + 1);
|
|
583
|
-
runDeliberationPipeline(
|
|
587
|
+
runDeliberationPipeline();
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
// r.action === "route_normally" — team directed chat.
|
|
591
|
+
// The user is drilling into a specific model. Use shared team context.
|
|
592
|
+
const targetModel = session.targetMode.type === "directed"
|
|
593
|
+
? session.targetMode.modelName
|
|
594
|
+
: null;
|
|
595
|
+
if (!targetModel)
|
|
596
|
+
return;
|
|
597
|
+
const tm = session.models.find((m) => m.name === targetModel && !m.muted);
|
|
598
|
+
if (!tm)
|
|
584
599
|
return;
|
|
600
|
+
const tmc = config.models[targetModel];
|
|
601
|
+
if (!tmc) {
|
|
602
|
+
tm.buffer = `[Error: No config for model "${targetModel}"]`;
|
|
603
|
+
setModelStates([...session.models]);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
// Worktree setup
|
|
607
|
+
const taskId = Date.now().toString(36);
|
|
608
|
+
const wtManager = new WorktreeManager(process.cwd());
|
|
609
|
+
await wtManager.setup(taskId, [targetModel]);
|
|
610
|
+
const wtPath = wtManager.getWorktreePath(targetModel) ?? process.cwd();
|
|
611
|
+
tm.isStreaming = true;
|
|
612
|
+
tm.buffer = "";
|
|
613
|
+
setModelStates([...session.models]);
|
|
614
|
+
const stream = runTurn({
|
|
615
|
+
modelName: targetModel,
|
|
616
|
+
config: tmc,
|
|
617
|
+
messages: session.teamMessages,
|
|
618
|
+
systemPrompt: makeSystemPrompt(targetModel, tmc.provider),
|
|
619
|
+
tools: toolRegistry.getDefinitions(),
|
|
620
|
+
registry: toolRegistry,
|
|
621
|
+
permission: permissionManager,
|
|
622
|
+
worktreePath: wtPath,
|
|
623
|
+
});
|
|
624
|
+
for await (const event of stream) {
|
|
625
|
+
if (event.type === "text") {
|
|
626
|
+
tm.buffer += event.content;
|
|
627
|
+
}
|
|
628
|
+
else if (event.type === "done") {
|
|
629
|
+
tm.usage.input += event.usage.input;
|
|
630
|
+
tm.usage.output += event.usage.output;
|
|
631
|
+
tm.isStreaming = false;
|
|
632
|
+
}
|
|
633
|
+
else if (event.type === "error") {
|
|
634
|
+
tm.buffer += `\n[Error: ${event.message}]`;
|
|
635
|
+
tm.isStreaming = false;
|
|
636
|
+
}
|
|
637
|
+
setModelStates([...session.models]);
|
|
585
638
|
}
|
|
586
|
-
|
|
639
|
+
await wtManager.cleanup(taskId);
|
|
640
|
+
saveCurrentSession();
|
|
641
|
+
return;
|
|
587
642
|
}
|
|
588
643
|
// ── Team mode toggle ────────────────────────────────────
|
|
589
644
|
if (trimmed === "/team" || trimmed === "/t") {
|
|
@@ -718,6 +773,6 @@ broadcast = true`;
|
|
|
718
773
|
"\u26A0 ",
|
|
719
774
|
w.message))))),
|
|
720
775
|
React.createElement(Box, { flexGrow: 1 },
|
|
721
|
-
React.createElement(OutputArea, { models: modelStates, targetMode: session.targetMode, scrollOffsets: scrollOffsets, comparisonModel: comparisonModel, terminalWidth: terminalWidth, deliberationProgress: deliberationProgress, deliberationDocument: deliberationDocument, deliberationRounds: deliberationRounds, teamMode: teamMode, deliberationScrollOffset: deliberationScrollOffset })),
|
|
776
|
+
React.createElement(OutputArea, { models: modelStates, targetMode: session.targetMode, scrollOffsets: scrollOffsets, comparisonModel: comparisonModel, terminalWidth: terminalWidth, deliberationProgress: deliberationProgress, deliberationDocument: deliberationDocument, deliberationThinkText: deliberationThinkText, deliberationRounds: deliberationRounds, teamMode: teamMode, deliberationScrollOffset: deliberationScrollOffset })),
|
|
722
777
|
React.createElement(InputBar, { models: modelStates, activeModelName: activeModelName, prefix: targetPrefix, value: input, onChange: handleInputChange, onSubmit: handleSubmit })));
|
|
723
778
|
};
|
|
@@ -11,11 +11,12 @@ function spinner(frame) {
|
|
|
11
11
|
const chars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
12
12
|
return chars[frame % chars.length] ?? ".";
|
|
13
13
|
}
|
|
14
|
-
export const DeliberationView = ({ progress, document, rounds, scrollOffset = 0 }) => {
|
|
14
|
+
export const DeliberationView = ({ progress, document, thinkText = "", rounds, scrollOffset = 0 }) => {
|
|
15
15
|
const role = progress.role ?? "draft";
|
|
16
16
|
const roleColor = ROLE_COLORS[role];
|
|
17
17
|
const isActive = progress.type !== "done" && progress.type !== "error";
|
|
18
18
|
const isDone = progress.type === "done";
|
|
19
|
+
const isThinking = progress.type === "think_start" || progress.type === "think_text";
|
|
19
20
|
const spin = spinner(Date.now() % 10);
|
|
20
21
|
const allLines = document ? document.split("\n") : [];
|
|
21
22
|
const lines = allLines.slice(scrollOffset);
|
|
@@ -40,12 +41,17 @@ export const DeliberationView = ({ progress, document, rounds, scrollOffset = 0
|
|
|
40
41
|
React.createElement(Box, { flexDirection: "row" },
|
|
41
42
|
React.createElement(Text, { dimColor: true }, r.modelName))));
|
|
42
43
|
})),
|
|
43
|
-
isActive && (React.createElement(Box, { marginBottom: 1 },
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
isActive && (React.createElement(Box, { marginBottom: 1 }, isThinking ? (React.createElement(Text, { dimColor: true },
|
|
45
|
+
"\uD83D\uDCAD \u601D\u8003\u4E2D \u2014 ",
|
|
46
|
+
progress.modelName,
|
|
47
|
+
" \u2014 \u5206\u6790\u5F53\u524D\u72B6\u6001\u2026")) : (React.createElement(Text, { color: roleColor },
|
|
48
|
+
roundLabel(role),
|
|
49
|
+
"\u4E2D \u2014 ",
|
|
50
|
+
progress.modelName,
|
|
51
|
+
" \u2014 \u6B63\u5728\u751F\u6210\u2026")))),
|
|
52
|
+
thinkText && isThinking && (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
53
|
+
React.createElement(Text, { dimColor: true }, "\u2500\u2500 \u79C1\u6709\u5206\u6790 \u2500\u2500"),
|
|
54
|
+
thinkText.split("\n").map((line, i) => (React.createElement(Text, { key: i, dimColor: true }, line || " "))))),
|
|
49
55
|
progress.type === "error" && (React.createElement(Box, { marginBottom: 1 },
|
|
50
56
|
React.createElement(Text, { color: "red" },
|
|
51
57
|
"\u9519\u8BEF\uFF1A",
|
|
@@ -15,6 +15,7 @@ interface Props {
|
|
|
15
15
|
terminalWidth: number;
|
|
16
16
|
deliberationProgress?: DeliberationProgress | null;
|
|
17
17
|
deliberationDocument?: string;
|
|
18
|
+
deliberationThinkText?: string;
|
|
18
19
|
deliberationRounds?: RoundSummary[];
|
|
19
20
|
teamMode?: boolean;
|
|
20
21
|
deliberationScrollOffset?: number;
|
|
@@ -3,7 +3,7 @@ import { Box, Text } from "ink";
|
|
|
3
3
|
import { BroadcastSummary } from "./BroadcastSummary.js";
|
|
4
4
|
import { ModelDetail } from "./ModelDetail.js";
|
|
5
5
|
import { DeliberationView } from "./DeliberationView.js";
|
|
6
|
-
export const OutputArea = ({ models, targetMode, scrollOffsets, comparisonModel, terminalWidth, deliberationProgress, deliberationDocument, deliberationRounds, teamMode, deliberationScrollOffset = 0, }) => {
|
|
6
|
+
export const OutputArea = ({ models, targetMode, scrollOffsets, comparisonModel, terminalWidth, deliberationProgress, deliberationDocument, deliberationThinkText, deliberationRounds, teamMode, deliberationScrollOffset = 0, }) => {
|
|
7
7
|
// ── Team mode ──────────────────────────────────────────────────
|
|
8
8
|
if (teamMode) {
|
|
9
9
|
// During active deliberation (running): always show deliberation view
|
|
@@ -11,12 +11,12 @@ export const OutputArea = ({ models, targetMode, scrollOffsets, comparisonModel,
|
|
|
11
11
|
deliberationProgress.type !== "done" &&
|
|
12
12
|
deliberationProgress.type !== "error";
|
|
13
13
|
if (isDeliberating) {
|
|
14
|
-
return (React.createElement(DeliberationView, { progress: deliberationProgress, scrollOffset: deliberationScrollOffset, document: deliberationDocument ?? "", rounds: deliberationRounds ?? [] }));
|
|
14
|
+
return (React.createElement(DeliberationView, { progress: deliberationProgress, scrollOffset: deliberationScrollOffset, document: deliberationDocument ?? "", thinkText: deliberationThinkText ?? "", rounds: deliberationRounds ?? [] }));
|
|
15
15
|
}
|
|
16
16
|
// Team overview (broadcast target): show deliberation result or idle prompt
|
|
17
17
|
if (targetMode.type === "broadcast") {
|
|
18
18
|
if (deliberationProgress) {
|
|
19
|
-
return (React.createElement(DeliberationView, { progress: deliberationProgress, document: deliberationDocument ?? "", rounds: deliberationRounds ?? [], scrollOffset: deliberationScrollOffset }));
|
|
19
|
+
return (React.createElement(DeliberationView, { progress: deliberationProgress, document: deliberationDocument ?? "", thinkText: deliberationThinkText ?? "", rounds: deliberationRounds ?? [], scrollOffset: deliberationScrollOffset }));
|
|
20
20
|
}
|
|
21
21
|
return (React.createElement(Box, { flexDirection: "column", flexGrow: 1, padding: 1 },
|
|
22
22
|
React.createElement(Text, { bold: true }, "\u56E2\u961F\u6A21\u5F0F"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "multiarena",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Terminal-native multi-model content generation — N models collaborate to produce the best document, analysis, script, or strategy",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
],
|
|
32
32
|
"repository": {
|
|
33
33
|
"type": "git",
|
|
34
|
-
"url": "https://github.com/timgunnar/multiarena"
|
|
34
|
+
"url": "git+https://github.com/timgunnar/multiarena.git"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"prebuild": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|