ei-tui 0.6.7 → 0.7.1
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/src/cli/mcp.ts +35 -10
- package/src/cli/persona-filter.ts +42 -0
- package/src/cli.ts +18 -6
- package/src/core/handlers/human-extraction.ts +1 -0
- package/src/core/handlers/human-matching.ts +10 -0
- package/src/core/handlers/index.ts +2 -1
- package/src/core/handlers/persona-response.ts +5 -0
- package/src/core/handlers/utils.ts +4 -1
- package/src/core/orchestrators/ceremony.ts +2 -2
- package/src/core/orchestrators/human-extraction.ts +5 -0
- package/src/core/personas/opencode-agent.ts +1 -0
- package/src/core/processor.ts +22 -2
- package/src/core/prompt-context-builder.ts +40 -10
- package/src/core/queue-manager.ts +18 -0
- package/src/core/room-manager.ts +21 -4
- package/src/core/state/personas.ts +2 -2
- package/src/core/state-manager.ts +26 -0
- package/src/core/types/data-items.ts +1 -0
- package/src/core/types/enums.ts +1 -0
- package/src/core/types/integrations.ts +1 -0
- package/src/core/types/rooms.ts +2 -0
- package/src/integrations/claude-code/importer.ts +3 -57
- package/src/integrations/cursor/importer.ts +2 -52
- package/src/integrations/opencode/importer.ts +1 -0
- package/src/prompts/response/sections.ts +1 -1
- package/src/prompts/response/types.ts +1 -0
- package/src/prompts/room/index.ts +2 -2
- package/src/prompts/room/sections.ts +4 -4
- package/src/prompts/room/types.ts +4 -0
- package/tui/src/commands/activate.tsx +7 -6
- package/tui/src/commands/context.tsx +188 -2
- package/tui/src/components/CYPTreeOverlay.tsx +357 -0
- package/tui/src/components/MAPScoreOverlay.tsx +300 -0
- package/tui/src/components/MessageList.tsx +14 -3
- package/tui/src/components/RoomMessageList.tsx +15 -3
- package/tui/src/context/ei.tsx +20 -0
- package/tui/src/util/cyp-tree.ts +62 -0
- package/tui/src/util/yaml-context.ts +87 -1
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { useKeyboard } from "@opentui/solid";
|
|
2
|
+
import { For, createMemo, createSignal, onMount, onCleanup, createEffect } from "solid-js";
|
|
3
|
+
import type { RoomMessage, PersonaSummary } from "../../../src/core/types.js";
|
|
4
|
+
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
5
|
+
import { useKeyboardNav } from "../context/keyboard.js";
|
|
6
|
+
import { buildCYPTree, getSubtreeIds } from "../util/cyp-tree.js";
|
|
7
|
+
|
|
8
|
+
export interface CYPTreeOverlayProps {
|
|
9
|
+
roomId: string;
|
|
10
|
+
roomName?: string;
|
|
11
|
+
messages: RoomMessage[];
|
|
12
|
+
activeNodeId: string;
|
|
13
|
+
activeRoomPath: RoomMessage[];
|
|
14
|
+
personas: PersonaSummary[];
|
|
15
|
+
onSelectBranch: (messageId: string) => Promise<void>;
|
|
16
|
+
onDismiss: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type LineKind = 'normal' | 'masked' | 'your-turn';
|
|
20
|
+
|
|
21
|
+
interface TreeLine {
|
|
22
|
+
messageId: string;
|
|
23
|
+
globalNum: number;
|
|
24
|
+
prefix: string;
|
|
25
|
+
speaker: string;
|
|
26
|
+
preview: string;
|
|
27
|
+
stateIndicator: string;
|
|
28
|
+
kind: LineKind;
|
|
29
|
+
isMaskedOrPlaceholder: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getMessageContent(m: RoomMessage): string {
|
|
33
|
+
if (m.content) return m.content;
|
|
34
|
+
if (m.verbal_response) return m.verbal_response;
|
|
35
|
+
if (m.action_response) return m.action_response;
|
|
36
|
+
if (m.silence_reason) return `(${m.silence_reason})`;
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function CYPTreeOverlay(props: CYPTreeOverlayProps) {
|
|
41
|
+
const { setOverlayActive } = useKeyboardNav();
|
|
42
|
+
onMount(() => setOverlayActive(true));
|
|
43
|
+
onCleanup(() => setOverlayActive(false));
|
|
44
|
+
|
|
45
|
+
const treeData = createMemo(() => buildCYPTree(props.messages));
|
|
46
|
+
|
|
47
|
+
const [highlightedId, setHighlightedId] = createSignal<string>(props.activeNodeId);
|
|
48
|
+
const [viewStack, setViewStack] = createSignal<string[]>([]);
|
|
49
|
+
const [numBuffer, setNumBuffer] = createSignal<string>("");
|
|
50
|
+
const [navError, setNavError] = createSignal<string>("");
|
|
51
|
+
|
|
52
|
+
let scrollRef: ScrollBoxRenderable | null = null;
|
|
53
|
+
|
|
54
|
+
createEffect(() => {
|
|
55
|
+
const hid = highlightedId();
|
|
56
|
+
const lines = visibleLines();
|
|
57
|
+
const idx = lines.findIndex(l => l.messageId === hid);
|
|
58
|
+
if (idx < 0 || !scrollRef) return;
|
|
59
|
+
const top = scrollRef.scrollTop;
|
|
60
|
+
const visible = scrollRef.height;
|
|
61
|
+
if (idx < top) {
|
|
62
|
+
scrollRef.scrollTo(idx);
|
|
63
|
+
} else if (idx >= top + visible) {
|
|
64
|
+
scrollRef.scrollTo(idx - visible + 1);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const activeRoomPathIds = createMemo(() => new Set(props.activeRoomPath.map((m) => m.id)));
|
|
69
|
+
|
|
70
|
+
const incompleteFamilies = createMemo<Set<string>>(() => {
|
|
71
|
+
const result = new Set<string>();
|
|
72
|
+
const { childrenMap } = treeData();
|
|
73
|
+
childrenMap.forEach((children, parentId) => {
|
|
74
|
+
const hasPersonaChild = children.some(c => c.role === "persona");
|
|
75
|
+
const hasHumanChild = children.some(c => c.role === "human");
|
|
76
|
+
if (hasPersonaChild && !hasHumanChild) result.add(parentId);
|
|
77
|
+
});
|
|
78
|
+
return result;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const ICON_COL = 74;
|
|
82
|
+
const NUM_WIDTH = 7;
|
|
83
|
+
|
|
84
|
+
const visibleLines = createMemo<TreeLine[]>(() => {
|
|
85
|
+
const { ordered, idToNum, childrenMap } = treeData();
|
|
86
|
+
if (ordered.length === 0) return [];
|
|
87
|
+
|
|
88
|
+
const stack = viewStack();
|
|
89
|
+
const viewRootId = stack.length > 0 ? stack[stack.length - 1] : null;
|
|
90
|
+
const subtreeIds = viewRootId ? getSubtreeIds(viewRootId, childrenMap) : null;
|
|
91
|
+
const incomplete = incompleteFamilies();
|
|
92
|
+
|
|
93
|
+
const lines: TreeLine[] = [];
|
|
94
|
+
|
|
95
|
+
function buildPrefix(ancestorIsLast: boolean[], isLast: boolean, isDisplayRoot: boolean): string {
|
|
96
|
+
if (isDisplayRoot) return "";
|
|
97
|
+
let p = "";
|
|
98
|
+
for (const anc of ancestorIsLast) p += anc ? " " : "\u2502 ";
|
|
99
|
+
return p + (isLast ? "\u2514\u2500 " : "\u251C\u2500 ");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function formatLine(numStr: string, prefix: string, speaker: string, content: string, icon: string): string {
|
|
103
|
+
const prefixLen = prefix.length;
|
|
104
|
+
const available = ICON_COL - NUM_WIDTH - prefixLen - speaker.length - 4 - 1;
|
|
105
|
+
const trimmed = content.length > available ? content.slice(0, Math.max(available - 1, 8)) + "\u2026" : content;
|
|
106
|
+
const body = `${numStr} ${prefix}${speaker}: "${trimmed}"`;
|
|
107
|
+
return body.padEnd(ICON_COL) + " " + icon;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function pushLine(msgId: string, prefix: string, speaker: string, content: string, icon: string, kind: LineKind, globalNum: number) {
|
|
111
|
+
const numStr = ` ${String(globalNum).padStart(3)} `;
|
|
112
|
+
const formatted = formatLine(numStr, prefix, speaker, content, icon);
|
|
113
|
+
lines.push({
|
|
114
|
+
messageId: msgId,
|
|
115
|
+
globalNum,
|
|
116
|
+
prefix,
|
|
117
|
+
speaker,
|
|
118
|
+
preview: formatted,
|
|
119
|
+
stateIndicator: icon,
|
|
120
|
+
kind,
|
|
121
|
+
isMaskedOrPlaceholder: kind === 'masked' || kind === 'your-turn',
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function visit(msgId: string, isLast: boolean, ancestorIsLast: boolean[], isDisplayRoot: boolean) {
|
|
126
|
+
const msg = props.messages.find((m) => m.id === msgId);
|
|
127
|
+
if (!msg) return;
|
|
128
|
+
if (subtreeIds && !subtreeIds.has(msgId)) return;
|
|
129
|
+
|
|
130
|
+
const children = childrenMap.get(msgId) ?? [];
|
|
131
|
+
const prefix = buildPrefix(ancestorIsLast, isLast, isDisplayRoot);
|
|
132
|
+
const globalNum = idToNum.get(msgId) ?? 0;
|
|
133
|
+
|
|
134
|
+
let kind: LineKind = 'normal';
|
|
135
|
+
if (msg.parent_id && incomplete.has(msg.parent_id)) kind = 'masked';
|
|
136
|
+
|
|
137
|
+
let icon: string;
|
|
138
|
+
if (msgId === props.activeNodeId) icon = "\u25CF";
|
|
139
|
+
else if (activeRoomPathIds().has(msgId)) icon = "\u25CB";
|
|
140
|
+
else if (kind === 'masked') icon = "\uD83D\uDD12";
|
|
141
|
+
else if (children.length === 0) icon = "\u00B7";
|
|
142
|
+
else icon = "\u25CB";
|
|
143
|
+
|
|
144
|
+
let speaker = "You";
|
|
145
|
+
if (msg.role === "persona" && msg.persona_id) {
|
|
146
|
+
speaker = props.personas.find((p) => p.id === msg.persona_id)?.display_name ?? msg.persona_id;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const content = kind === 'masked' ? "[hidden]" : getMessageContent(msg).replace(/\n/g, " ");
|
|
150
|
+
pushLine(msgId, prefix, speaker, content, icon, kind, globalNum);
|
|
151
|
+
|
|
152
|
+
const nextAncestors = isDisplayRoot ? [] : [...ancestorIsLast, isLast];
|
|
153
|
+
const hasHumanChild = children.some(c => c.role === "human");
|
|
154
|
+
const hasPersonaChild = children.some(c => c.role === "persona");
|
|
155
|
+
const needsYourTurn = hasPersonaChild && !hasHumanChild;
|
|
156
|
+
|
|
157
|
+
for (let i = 0; i < children.length; i++) {
|
|
158
|
+
const childIsLast = !needsYourTurn && i === children.length - 1;
|
|
159
|
+
visit(children[i].id, childIsLast, nextAncestors, false);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (needsYourTurn) {
|
|
163
|
+
const ytPrefix = buildPrefix(nextAncestors, true, false);
|
|
164
|
+
const numStr = ` -- `;
|
|
165
|
+
const formatted = formatLine(numStr, ytPrefix, "You", "[Your turn]", "\u270F\uFE0F");
|
|
166
|
+
lines.push({
|
|
167
|
+
messageId: `your-turn-${msgId}`,
|
|
168
|
+
globalNum: 0,
|
|
169
|
+
prefix: ytPrefix,
|
|
170
|
+
speaker: "You",
|
|
171
|
+
preview: formatted,
|
|
172
|
+
stateIndicator: "\u270F\uFE0F",
|
|
173
|
+
kind: 'your-turn',
|
|
174
|
+
isMaskedOrPlaceholder: true,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const displayRootId = viewRootId ?? (ordered[0]?.id ?? null);
|
|
180
|
+
if (displayRootId) visit(displayRootId, true, [], true);
|
|
181
|
+
|
|
182
|
+
return lines;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
useKeyboard((event) => {
|
|
186
|
+
event.preventDefault();
|
|
187
|
+
const key = event.name;
|
|
188
|
+
|
|
189
|
+
if (navError()) setNavError("");
|
|
190
|
+
|
|
191
|
+
if (key === "q" || key === "escape") {
|
|
192
|
+
props.onDismiss();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (key === "o") {
|
|
197
|
+
setViewStack((prev) => prev.slice(0, -1));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (key === "i") {
|
|
202
|
+
const hid = highlightedId();
|
|
203
|
+
if (hid) {
|
|
204
|
+
setViewStack((prev) => [...prev, hid]);
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (key === "up" || key === "k") {
|
|
210
|
+
const lines = visibleLines();
|
|
211
|
+
const idx = lines.findIndex((l) => l.messageId === highlightedId());
|
|
212
|
+
if (idx > 0) setHighlightedId(lines[idx - 1].messageId);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (key === "down" || key === "j") {
|
|
217
|
+
const lines = visibleLines();
|
|
218
|
+
const idx = lines.findIndex((l) => l.messageId === highlightedId());
|
|
219
|
+
if (idx < lines.length - 1) setHighlightedId(lines[idx + 1].messageId);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (key === "pageup") {
|
|
224
|
+
if (scrollRef) {
|
|
225
|
+
const half = Math.max(1, Math.floor(scrollRef.height / 2));
|
|
226
|
+
scrollRef.scrollBy(-half);
|
|
227
|
+
const lines = visibleLines();
|
|
228
|
+
const newTop = scrollRef.scrollTop;
|
|
229
|
+
const targetLine = lines[newTop];
|
|
230
|
+
if (targetLine) setHighlightedId(targetLine.messageId);
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (key === "pagedown") {
|
|
236
|
+
if (scrollRef) {
|
|
237
|
+
const half = Math.max(1, Math.floor(scrollRef.height / 2));
|
|
238
|
+
scrollRef.scrollBy(half);
|
|
239
|
+
const lines = visibleLines();
|
|
240
|
+
const newTop = scrollRef.scrollTop;
|
|
241
|
+
const targetLine = lines[Math.min(newTop + scrollRef.height - 1, lines.length - 1)];
|
|
242
|
+
if (targetLine) setHighlightedId(targetLine.messageId);
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (key === "backspace") {
|
|
248
|
+
setNumBuffer((prev) => prev.slice(0, -1));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (key === "return") {
|
|
253
|
+
const buf = numBuffer();
|
|
254
|
+
let targetId: string | undefined;
|
|
255
|
+
|
|
256
|
+
if (buf.length > 0) {
|
|
257
|
+
const num = parseInt(buf, 10);
|
|
258
|
+
const { numToId } = treeData();
|
|
259
|
+
targetId = numToId.get(num);
|
|
260
|
+
if (!targetId) {
|
|
261
|
+
setNavError(`No node at position ${num}`);
|
|
262
|
+
setNumBuffer("");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
const hid = highlightedId();
|
|
267
|
+
const line = visibleLines().find(l => l.messageId === hid);
|
|
268
|
+
if (!line || line.isMaskedOrPlaceholder) return;
|
|
269
|
+
targetId = hid;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (targetId) {
|
|
273
|
+
const targetMsg = props.messages.find(m => m.id === targetId);
|
|
274
|
+
if (targetMsg?.parent_id && incompleteFamilies().has(targetMsg.parent_id)) {
|
|
275
|
+
setNavError(`Node is masked — a sibling response is missing`);
|
|
276
|
+
setNumBuffer("");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
setNavError("");
|
|
280
|
+
setNumBuffer("");
|
|
281
|
+
void props.onSelectBranch(targetId).then(() => props.onDismiss());
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (key && key.length === 1 && key >= "0" && key <= "9") {
|
|
287
|
+
setNumBuffer((prev) => prev + key);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const headerText = createMemo(() => {
|
|
293
|
+
const name = props.roomName ?? props.roomId;
|
|
294
|
+
return `${name} \u25CF active \u25CB activated \u00B7 unexplored \uD83D\uDD12 masked \u270F\uFE0F your turn`;
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const footerText = createMemo(() => {
|
|
298
|
+
const err = navError();
|
|
299
|
+
if (err) return `! ${err} (press any key to continue)`;
|
|
300
|
+
const buf = numBuffer();
|
|
301
|
+
const bufPart = buf.length > 0 ? ` | #${buf}_` : "";
|
|
302
|
+
return `[i] zoom in [o] zoom out [q] quit | Type number or highlight + Enter to activate${bufPart}`;
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
<box
|
|
307
|
+
position="absolute"
|
|
308
|
+
width="100%"
|
|
309
|
+
height="100%"
|
|
310
|
+
left={0}
|
|
311
|
+
top={0}
|
|
312
|
+
backgroundColor="#000000"
|
|
313
|
+
alignItems="center"
|
|
314
|
+
justifyContent="center"
|
|
315
|
+
>
|
|
316
|
+
<box
|
|
317
|
+
width="95%"
|
|
318
|
+
height="95%"
|
|
319
|
+
backgroundColor="#1a1a2e"
|
|
320
|
+
borderStyle="single"
|
|
321
|
+
borderColor="#586e75"
|
|
322
|
+
flexDirection="column"
|
|
323
|
+
>
|
|
324
|
+
<box paddingLeft={1} paddingRight={1} paddingTop={1}>
|
|
325
|
+
<text fg="#eee8d5">{headerText()}</text>
|
|
326
|
+
</box>
|
|
327
|
+
|
|
328
|
+
<scrollbox height="100%" marginTop={1} marginBottom={1} ref={(el: ScrollBoxRenderable) => { scrollRef = el; }}>
|
|
329
|
+
<For each={visibleLines()}>
|
|
330
|
+
{(line) => {
|
|
331
|
+
const isHighlighted = () => line.messageId === highlightedId();
|
|
332
|
+
const fg = () => {
|
|
333
|
+
if (isHighlighted()) return "#b58900";
|
|
334
|
+
if (line.kind === 'masked') return "#44475a";
|
|
335
|
+
if (line.kind === 'your-turn') return "#6272a4";
|
|
336
|
+
return "#93a1a1";
|
|
337
|
+
};
|
|
338
|
+
return (
|
|
339
|
+
<box
|
|
340
|
+
visible={true}
|
|
341
|
+
backgroundColor={isHighlighted() ? "#2d3748" : "transparent"}
|
|
342
|
+
paddingLeft={1}
|
|
343
|
+
>
|
|
344
|
+
<text fg={fg()}>{line.preview}</text>
|
|
345
|
+
</box>
|
|
346
|
+
);
|
|
347
|
+
}}
|
|
348
|
+
</For>
|
|
349
|
+
</scrollbox>
|
|
350
|
+
|
|
351
|
+
<box paddingLeft={1} paddingRight={1} paddingBottom={1}>
|
|
352
|
+
<text fg={navError() ? "#dc322f" : "#586e75"}>{footerText()}</text>
|
|
353
|
+
</box>
|
|
354
|
+
</box>
|
|
355
|
+
</box>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { useKeyboard } from "@opentui/solid";
|
|
2
|
+
import { For, createMemo, onMount, onCleanup } from "solid-js";
|
|
3
|
+
import type { RoomMessage, PersonaSummary } from "../../../src/core/types.js";
|
|
4
|
+
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
5
|
+
import { useKeyboardNav } from "../context/keyboard.js";
|
|
6
|
+
|
|
7
|
+
export interface MAPScoreOverlayProps {
|
|
8
|
+
roomId: string;
|
|
9
|
+
roomName?: string;
|
|
10
|
+
messages: RoomMessage[];
|
|
11
|
+
activeNodeId: string;
|
|
12
|
+
activeRoomPath: RoomMessage[];
|
|
13
|
+
personas: PersonaSummary[];
|
|
14
|
+
judgePersonaId: string;
|
|
15
|
+
humanName: string;
|
|
16
|
+
onDismiss: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ScoreRow {
|
|
20
|
+
round: number | null; // null = initial seed message
|
|
21
|
+
winnerId: string | null;
|
|
22
|
+
winnerName: string;
|
|
23
|
+
winnerScore: number; // total wins for winner at this round
|
|
24
|
+
messagePreview: string;
|
|
25
|
+
verdict: string;
|
|
26
|
+
isInProgress: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function truncate(s: string, n: number): string {
|
|
30
|
+
if (s.length <= n) return s;
|
|
31
|
+
return s.slice(0, n - 1) + "…";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getMessageText(m: RoomMessage): string {
|
|
35
|
+
return (m.verbal_response ?? m.content ?? m.silence_reason ?? "").replace(/\n+/g, " ");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function MAPScoreOverlay(props: MAPScoreOverlayProps) {
|
|
39
|
+
const { setOverlayActive } = useKeyboardNav();
|
|
40
|
+
onMount(() => setOverlayActive(true));
|
|
41
|
+
onCleanup(() => setOverlayActive(false));
|
|
42
|
+
|
|
43
|
+
let scrollRef: ScrollBoxRenderable | null = null;
|
|
44
|
+
|
|
45
|
+
const personaMap = createMemo(() => {
|
|
46
|
+
const m = new Map<string, string>();
|
|
47
|
+
for (const p of props.personas) m.set(p.id, p.display_name);
|
|
48
|
+
return m;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Active path: walk parent_id chain from active_node_id to root, then reverse.
|
|
52
|
+
// We can't use getRoomActivePath from state-manager here (TUI component), so we derive it.
|
|
53
|
+
const activePath = createMemo<RoomMessage[]>(() => {
|
|
54
|
+
const msgById = new Map<string, RoomMessage>();
|
|
55
|
+
for (const m of props.messages) msgById.set(m.id, m);
|
|
56
|
+
|
|
57
|
+
const chain: RoomMessage[] = [];
|
|
58
|
+
let cur = msgById.get(props.activeNodeId);
|
|
59
|
+
while (cur) {
|
|
60
|
+
chain.push(cur);
|
|
61
|
+
cur = cur.parent_id ? msgById.get(cur.parent_id) : undefined;
|
|
62
|
+
}
|
|
63
|
+
return chain.reverse();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const scoreRows = createMemo<ScoreRow[]>(() => {
|
|
67
|
+
const path = activePath();
|
|
68
|
+
const pMap = personaMap();
|
|
69
|
+
const judgeId = props.judgePersonaId;
|
|
70
|
+
|
|
71
|
+
if (path.length === 0) return [];
|
|
72
|
+
|
|
73
|
+
const msgById = new Map<string, RoomMessage>();
|
|
74
|
+
for (const m of props.messages) msgById.set(m.id, m);
|
|
75
|
+
|
|
76
|
+
// Verdicts are siblings of winners — same parent_id as the winner.
|
|
77
|
+
// They are NOT on the active path, so index all messages by parent_id.
|
|
78
|
+
const verdictByParentId = new Map<string, RoomMessage>();
|
|
79
|
+
for (const m of props.messages) {
|
|
80
|
+
if (m.persona_id === judgeId && m.silence_reason && m.parent_id) {
|
|
81
|
+
verdictByParentId.set(m.parent_id, m);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const winCounts = new Map<string, number>();
|
|
86
|
+
const rows: ScoreRow[] = [];
|
|
87
|
+
let round = 0;
|
|
88
|
+
|
|
89
|
+
const seed = path[0];
|
|
90
|
+
if (!seed) return rows;
|
|
91
|
+
|
|
92
|
+
rows.push({
|
|
93
|
+
round: null,
|
|
94
|
+
winnerId: null,
|
|
95
|
+
winnerName: "—",
|
|
96
|
+
winnerScore: 0,
|
|
97
|
+
messagePreview: truncate(getMessageText(seed), 50),
|
|
98
|
+
verdict: "(initial)",
|
|
99
|
+
isInProgress: false,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Active path: human → winner-1 → winner-2 → ...
|
|
103
|
+
// Each non-judge persona message on the path is a round winner.
|
|
104
|
+
for (const msg of path) {
|
|
105
|
+
if (msg.role !== "persona" || msg.persona_id === judgeId) continue;
|
|
106
|
+
|
|
107
|
+
round++;
|
|
108
|
+
const winnerId = msg.persona_id ?? "";
|
|
109
|
+
const prev = winCounts.get(winnerId) ?? 0;
|
|
110
|
+
const newCount = prev + 1;
|
|
111
|
+
winCounts.set(winnerId, newCount);
|
|
112
|
+
const winnerName = pMap.get(winnerId) ?? winnerId.slice(0, 8);
|
|
113
|
+
const verdictMsg = msg.parent_id ? verdictByParentId.get(msg.parent_id) : undefined;
|
|
114
|
+
const verdict = verdictMsg ? truncate(verdictMsg.silence_reason ?? "", 50) : "";
|
|
115
|
+
|
|
116
|
+
rows.push({
|
|
117
|
+
round,
|
|
118
|
+
winnerId,
|
|
119
|
+
winnerName: `${winnerName} (${newCount})`,
|
|
120
|
+
winnerScore: newCount,
|
|
121
|
+
messagePreview: truncate(getMessageText(msg), 50),
|
|
122
|
+
verdict,
|
|
123
|
+
isInProgress: false,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return rows;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const scoreSummary = createMemo<string>(() => {
|
|
131
|
+
const path = activePath();
|
|
132
|
+
const pMap = personaMap();
|
|
133
|
+
const judgeId = props.judgePersonaId;
|
|
134
|
+
|
|
135
|
+
const counts = new Map<string, number>();
|
|
136
|
+
let humanWins = 0;
|
|
137
|
+
|
|
138
|
+
for (const m of path) {
|
|
139
|
+
if (m.role === "persona" && m.persona_id !== judgeId) {
|
|
140
|
+
const id = m.persona_id ?? "";
|
|
141
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const completedRounds = scoreRows().filter((r) => !r.isInProgress && r.round !== null).length;
|
|
146
|
+
if (completedRounds === 0) return "No rounds completed yet.";
|
|
147
|
+
|
|
148
|
+
const parts: string[] = [];
|
|
149
|
+
const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
|
150
|
+
for (const [id, cnt] of sorted) {
|
|
151
|
+
const name = pMap.get(id) ?? id.slice(0, 8);
|
|
152
|
+
parts.push(`${name} ${cnt}`);
|
|
153
|
+
}
|
|
154
|
+
parts.push(`${props.humanName} ${humanWins}`);
|
|
155
|
+
|
|
156
|
+
return "Score: " + parts.join(" — ");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
useKeyboard((event) => {
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
const key = event.name;
|
|
162
|
+
|
|
163
|
+
if (key === "q" || key === "escape") {
|
|
164
|
+
props.onDismiss();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (key === "up" || key === "k") {
|
|
169
|
+
scrollRef?.scrollBy(-1);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (key === "down" || key === "j") {
|
|
174
|
+
scrollRef?.scrollBy(1);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (key === "pageup") {
|
|
179
|
+
if (scrollRef) {
|
|
180
|
+
scrollRef.scrollBy(-Math.max(1, Math.floor(scrollRef.height / 2)));
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (key === "pagedown") {
|
|
186
|
+
if (scrollRef) {
|
|
187
|
+
scrollRef.scrollBy(Math.max(1, Math.floor(scrollRef.height / 2)));
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const COL_ROUND = 6;
|
|
194
|
+
const COL_WINNER = 16;
|
|
195
|
+
const COL_MSG = 36;
|
|
196
|
+
const COL_VERDICT = 36;
|
|
197
|
+
const SEP = " │ ";
|
|
198
|
+
|
|
199
|
+
const headerLine = createMemo(() => {
|
|
200
|
+
return (
|
|
201
|
+
"Round".padEnd(COL_ROUND) +
|
|
202
|
+
SEP +
|
|
203
|
+
"Winner".padEnd(COL_WINNER) +
|
|
204
|
+
SEP +
|
|
205
|
+
"Message".padEnd(COL_MSG) +
|
|
206
|
+
SEP +
|
|
207
|
+
"Verdict"
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const dividerLine = createMemo(() => {
|
|
212
|
+
return (
|
|
213
|
+
"─".repeat(COL_ROUND) +
|
|
214
|
+
"─┼─" +
|
|
215
|
+
"─".repeat(COL_WINNER) +
|
|
216
|
+
"─┼─" +
|
|
217
|
+
"─".repeat(COL_MSG) +
|
|
218
|
+
"─┼─" +
|
|
219
|
+
"─".repeat(COL_VERDICT)
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
function formatRow(row: ScoreRow): string {
|
|
224
|
+
const roundStr =
|
|
225
|
+
row.round === null ? " — " : String(row.round).padStart(3).padEnd(COL_ROUND);
|
|
226
|
+
const winner = row.winnerName.padEnd(COL_WINNER).slice(0, COL_WINNER);
|
|
227
|
+
const msg = row.messagePreview.padEnd(COL_MSG).slice(0, COL_MSG);
|
|
228
|
+
const verdict = row.verdict.padEnd(COL_VERDICT).slice(0, COL_VERDICT);
|
|
229
|
+
return roundStr + SEP + winner + SEP + msg + SEP + verdict;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function rowFg(row: ScoreRow): string {
|
|
233
|
+
if (row.isInProgress) return "#6272a4";
|
|
234
|
+
if (row.round === null) return "#586e75";
|
|
235
|
+
return "#93a1a1";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const roomTitle = createMemo(
|
|
239
|
+
() => `MAP Scoreboard — "${props.roomName ?? props.roomId}"`
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<box
|
|
244
|
+
position="absolute"
|
|
245
|
+
width="100%"
|
|
246
|
+
height="100%"
|
|
247
|
+
left={0}
|
|
248
|
+
top={0}
|
|
249
|
+
backgroundColor="#000000"
|
|
250
|
+
alignItems="center"
|
|
251
|
+
justifyContent="center"
|
|
252
|
+
>
|
|
253
|
+
<box
|
|
254
|
+
width="95%"
|
|
255
|
+
height="95%"
|
|
256
|
+
backgroundColor="#1a1a2e"
|
|
257
|
+
borderStyle="single"
|
|
258
|
+
borderColor="#586e75"
|
|
259
|
+
flexDirection="column"
|
|
260
|
+
>
|
|
261
|
+
<box paddingLeft={1} paddingRight={1} paddingTop={1}>
|
|
262
|
+
<text fg="#eee8d5">{roomTitle()}</text>
|
|
263
|
+
</box>
|
|
264
|
+
|
|
265
|
+
<box paddingLeft={1} paddingRight={1} marginTop={1} height={1}>
|
|
266
|
+
<text fg="#6272a4">{headerLine()}</text>
|
|
267
|
+
</box>
|
|
268
|
+
<box paddingLeft={1} paddingRight={1} height={1}>
|
|
269
|
+
<text fg="#44475a">{dividerLine()}</text>
|
|
270
|
+
</box>
|
|
271
|
+
|
|
272
|
+
<scrollbox
|
|
273
|
+
flexGrow={1}
|
|
274
|
+
ref={(el: ScrollBoxRenderable) => { scrollRef = el; }}
|
|
275
|
+
>
|
|
276
|
+
<For each={scoreRows()}>
|
|
277
|
+
{(row) => (
|
|
278
|
+
<box
|
|
279
|
+
visible={true}
|
|
280
|
+
paddingLeft={1}
|
|
281
|
+
>
|
|
282
|
+
<text fg={rowFg(row)}>{formatRow(row)}</text>
|
|
283
|
+
</box>
|
|
284
|
+
)}
|
|
285
|
+
</For>
|
|
286
|
+
</scrollbox>
|
|
287
|
+
|
|
288
|
+
<box paddingLeft={1} paddingRight={1} height={1}>
|
|
289
|
+
<text fg="#586e75">{"─".repeat(COL_ROUND + COL_WINNER + COL_MSG + COL_VERDICT + 9)}</text>
|
|
290
|
+
</box>
|
|
291
|
+
<box paddingLeft={1} paddingRight={1} height={1}>
|
|
292
|
+
<text fg="#b58900">{scoreSummary()}</text>
|
|
293
|
+
</box>
|
|
294
|
+
<box paddingLeft={1} paddingRight={1} height={1} marginBottom={1}>
|
|
295
|
+
<text fg="#586e75">{"[q] close [j/k] scroll [PgUp/PgDn] page"}</text>
|
|
296
|
+
</box>
|
|
297
|
+
</box>
|
|
298
|
+
</box>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { For, Show, createMemo, createSignal, createEffect, on } from "solid-js";
|
|
1
|
+
import { For, Show, createMemo, createSignal, createEffect, on, onMount } from "solid-js";
|
|
2
2
|
import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core";
|
|
3
3
|
import { useEi } from "../context/ei.js";
|
|
4
4
|
import { useKeyboardNav } from "../context/keyboard.js";
|
|
@@ -40,13 +40,24 @@ export function MessageList() {
|
|
|
40
40
|
const myId = ++instanceId;
|
|
41
41
|
logger.info(`MessageList instance ${myId} MOUNTED`);
|
|
42
42
|
|
|
43
|
-
const { messages, activePersonaId, personas, activeContextBoundary, getQuotes, quotesVersion } = useEi();
|
|
43
|
+
const { messages, activePersonaId, personas, activeContextBoundary, getQuotes, quotesVersion, getHuman } = useEi();
|
|
44
44
|
const { focusedPanel, registerMessageScroll } = useKeyboardNav();
|
|
45
45
|
|
|
46
46
|
const isFocused = () => focusedPanel() === "messages";
|
|
47
47
|
|
|
48
|
+
const [humanDisplayName, setHumanDisplayName] = createSignal("Human");
|
|
48
49
|
const [allQuotes, setAllQuotes] = createSignal<Quote[]>([]);
|
|
49
50
|
|
|
51
|
+
onMount(() => {
|
|
52
|
+
void getHuman().then(human => {
|
|
53
|
+
const name =
|
|
54
|
+
human.settings?.name_display ||
|
|
55
|
+
human.facts?.find(f => f.name === "Nickname/Preferred Name")?.description ||
|
|
56
|
+
"Human";
|
|
57
|
+
setHumanDisplayName(name);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
50
61
|
createEffect(on(() => [messages(), quotesVersion()], () => {
|
|
51
62
|
void getQuotes().then(setAllQuotes);
|
|
52
63
|
}));
|
|
@@ -112,7 +123,7 @@ export function MessageList() {
|
|
|
112
123
|
const persona = personas().find(p => p.id === activePersonaId());
|
|
113
124
|
return persona?.display_name ?? "Ei";
|
|
114
125
|
};
|
|
115
|
-
const speaker = message.role === "human" ?
|
|
126
|
+
const speaker = message.role === "human" ? humanDisplayName() : getDisplayName();
|
|
116
127
|
const speakerColor = message.role === "human" ? "#2aa198" : "#b58900";
|
|
117
128
|
|
|
118
129
|
const header = () => `${speaker} (${formatTime(message.timestamp)}) [✂️ ${message._quoteIndex}]:`;
|