ei-tui 0.6.6 → 0.7.0
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/README.md +16 -7
- package/src/cli/commands/people.ts +1 -0
- package/src/cli/mcp.ts +36 -11
- package/src/cli/persona-filter.ts +42 -0
- package/src/cli/retrieval.ts +3 -1
- package/src/cli.ts +18 -6
- package/src/core/handlers/human-extraction.ts +1 -0
- package/src/core/handlers/human-matching.ts +20 -4
- 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 +24 -7
- package/src/core/persona-manager.ts +3 -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-manager.ts +74 -0
- package/src/core/tools/builtin/read-memory.ts +1 -1
- package/src/core/types/data-items.ts +1 -0
- package/src/core/types/entities.ts +13 -0
- package/src/core/types/enums.ts +1 -0
- package/src/core/types/integrations.ts +4 -0
- package/src/core/types/rooms.ts +2 -0
- package/src/core/utils/identifier-utils.ts +24 -0
- package/src/core/utils/theme-codec.ts +78 -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,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}]:`;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { For, Show, createMemo, createSignal, createEffect, on, onCleanup } from "solid-js";
|
|
1
|
+
import { For, Show, createMemo, createSignal, createEffect, on, onCleanup, 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";
|
|
@@ -27,9 +27,21 @@ function formatTime(timestamp: string): string {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function RoomMessageList() {
|
|
30
|
-
const { roomMessages, roomActivePath, personas, activeRoomId, getRoom, getQuotes, quotesVersion } = useEi();
|
|
30
|
+
const { roomMessages, roomActivePath, personas, activeRoomId, getRoom, getQuotes, quotesVersion, getHuman } = useEi();
|
|
31
31
|
const { registerMessageScroll } = useKeyboardNav();
|
|
32
32
|
|
|
33
|
+
const [humanDisplayName, setHumanDisplayName] = createSignal("Human");
|
|
34
|
+
|
|
35
|
+
onMount(() => {
|
|
36
|
+
void getHuman().then(human => {
|
|
37
|
+
const name =
|
|
38
|
+
human.settings?.name_display ||
|
|
39
|
+
human.facts?.find(f => f.name === "Nickname/Preferred Name")?.description ||
|
|
40
|
+
"Human";
|
|
41
|
+
setHumanDisplayName(name);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
33
45
|
const personaNameMap = createMemo(() => {
|
|
34
46
|
const map = new Map<string, string>();
|
|
35
47
|
for (const p of personas()) {
|
|
@@ -99,7 +111,7 @@ export function RoomMessageList() {
|
|
|
99
111
|
});
|
|
100
112
|
|
|
101
113
|
const getSpeakerName = (msg: RoomMessage): string => {
|
|
102
|
-
if (msg.role === "human") return
|
|
114
|
+
if (msg.role === "human") return humanDisplayName();
|
|
103
115
|
if (msg.persona_id) return personaNameMap().get(msg.persona_id) ?? msg.persona_id;
|
|
104
116
|
return "Persona";
|
|
105
117
|
};
|
package/tui/src/context/ei.tsx
CHANGED
|
@@ -110,6 +110,8 @@ export interface EiContextValue {
|
|
|
110
110
|
dismissWelcomeOverlay: () => void;
|
|
111
111
|
deleteMessages: (personaId: string, messageIds: string[]) => Promise<void>;
|
|
112
112
|
setMessageContextStatus: (personaId: string, messageId: string, status: ContextStatus) => Promise<void>;
|
|
113
|
+
deleteRoomMessages: (roomId: string, messageIds: string[]) => Promise<void>;
|
|
114
|
+
setRoomMessageContextStatus: (roomId: string, messageId: string, status: ContextStatus) => Promise<void>;
|
|
113
115
|
recallPendingMessages: () => Promise<string>;
|
|
114
116
|
getToolProviderList: () => ToolProvider[];
|
|
115
117
|
getToolList: () => ToolDefinition[];
|
|
@@ -470,6 +472,22 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
470
472
|
setStore("messages", store.messages.map(m => m.id === messageId ? { ...m, context_status: status } : m));
|
|
471
473
|
};
|
|
472
474
|
|
|
475
|
+
const deleteRoomMessages = async (roomId: string, messageIds: string[]): Promise<void> => {
|
|
476
|
+
if (!processor) return;
|
|
477
|
+
processor.getStateManager().removeRoomMessages(roomId, messageIds);
|
|
478
|
+
if (roomId === store.activeRoomId) {
|
|
479
|
+
setStore("roomMessages", msgs => msgs.filter(m => !messageIds.includes(m.id)));
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const setRoomMessageContextStatus = async (roomId: string, messageId: string, status: ContextStatus): Promise<void> => {
|
|
484
|
+
if (!processor) return;
|
|
485
|
+
processor.getStateManager().updateRoomMessage(roomId, messageId, { context_status: status });
|
|
486
|
+
if (roomId === store.activeRoomId) {
|
|
487
|
+
setStore("roomMessages", msgs => msgs.map(m => m.id === messageId ? { ...m, context_status: status } : m));
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
|
|
473
491
|
const recallPendingMessages = async (): Promise<string> => {
|
|
474
492
|
if (!processor) return "";
|
|
475
493
|
const personaId = store.activePersonaId;
|
|
@@ -905,6 +923,8 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
905
923
|
dismissWelcomeOverlay: () => setShowWelcomeOverlay(false),
|
|
906
924
|
deleteMessages,
|
|
907
925
|
setMessageContextStatus,
|
|
926
|
+
deleteRoomMessages,
|
|
927
|
+
setRoomMessageContextStatus,
|
|
908
928
|
recallPendingMessages,
|
|
909
929
|
getToolProviderList,
|
|
910
930
|
getToolList,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { RoomMessage } from "../../../src/core/types.js";
|
|
2
|
+
|
|
3
|
+
export interface CYPTreeData {
|
|
4
|
+
ordered: RoomMessage[];
|
|
5
|
+
numToId: Map<number, string>;
|
|
6
|
+
idToNum: Map<string, number>;
|
|
7
|
+
childrenMap: Map<string, RoomMessage[]>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function buildCYPTree(messages: RoomMessage[]): CYPTreeData {
|
|
11
|
+
const childrenMap = new Map<string, RoomMessage[]>();
|
|
12
|
+
|
|
13
|
+
for (const m of messages) {
|
|
14
|
+
if (m.parent_id !== null && m.parent_id !== undefined) {
|
|
15
|
+
const existing = childrenMap.get(m.parent_id);
|
|
16
|
+
if (existing) {
|
|
17
|
+
existing.push(m);
|
|
18
|
+
} else {
|
|
19
|
+
childrenMap.set(m.parent_id, [m]);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const root = messages.find((m) => m.parent_id === null);
|
|
25
|
+
const ordered: RoomMessage[] = [];
|
|
26
|
+
const numToId = new Map<number, string>();
|
|
27
|
+
const idToNum = new Map<string, number>();
|
|
28
|
+
|
|
29
|
+
if (!root) {
|
|
30
|
+
return { ordered, numToId, idToNum, childrenMap };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const queue: RoomMessage[] = [root];
|
|
34
|
+
while (queue.length > 0) {
|
|
35
|
+
const current = queue.shift()!;
|
|
36
|
+
ordered.push(current);
|
|
37
|
+
const num = ordered.length;
|
|
38
|
+
numToId.set(num, current.id);
|
|
39
|
+
idToNum.set(current.id, num);
|
|
40
|
+
for (const child of childrenMap.get(current.id) ?? []) {
|
|
41
|
+
queue.push(child);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { ordered, numToId, idToNum, childrenMap };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getSubtreeIds(
|
|
49
|
+
rootId: string,
|
|
50
|
+
childrenMap: Map<string, RoomMessage[]>
|
|
51
|
+
): Set<string> {
|
|
52
|
+
const result = new Set<string>();
|
|
53
|
+
const queue = [rootId];
|
|
54
|
+
while (queue.length > 0) {
|
|
55
|
+
const id = queue.shift()!;
|
|
56
|
+
result.add(id);
|
|
57
|
+
for (const child of childrenMap.get(id) ?? []) {
|
|
58
|
+
queue.push(child.id);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import YAML from "yaml";
|
|
2
|
-
import type { Message } from "../../../src/core/types.js";
|
|
2
|
+
import type { Message, RoomMessage } from "../../../src/core/types.js";
|
|
3
3
|
import { ContextStatus } from "../../../src/core/types.js";
|
|
4
4
|
|
|
5
5
|
interface EditableMessage {
|
|
@@ -63,4 +63,90 @@ export function contextFromYAML(yamlContent: string): ContextYAMLResult {
|
|
|
63
63
|
return { messages, deletedMessageIds };
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
interface FfaEditableNode {
|
|
67
|
+
id: string;
|
|
68
|
+
role: "human" | "persona";
|
|
69
|
+
speaker?: string;
|
|
70
|
+
context_status: ContextStatus;
|
|
71
|
+
_delete?: boolean;
|
|
72
|
+
content?: string;
|
|
73
|
+
silence_reason?: string;
|
|
74
|
+
children?: FfaEditableNode[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildNode(msg: RoomMessage, messages: RoomMessage[], speakerMap: Map<string, string>, isRoot = false): FfaEditableNode {
|
|
78
|
+
const children = messages
|
|
79
|
+
.filter((m) => m.parent_id === msg.id)
|
|
80
|
+
.map((child) => buildNode(child, messages, speakerMap));
|
|
81
|
+
|
|
82
|
+
const node: FfaEditableNode = {
|
|
83
|
+
id: msg.id,
|
|
84
|
+
role: msg.role === "human" ? "human" : "persona",
|
|
85
|
+
context_status: msg.context_status,
|
|
86
|
+
...(!isRoot && { _delete: false }),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if (msg.role === "persona" && msg.persona_id) {
|
|
90
|
+
node.speaker = speakerMap.get(msg.persona_id) ?? msg.persona_id.slice(0, 8);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const text = getContent(msg);
|
|
94
|
+
if (text) node.content = text;
|
|
95
|
+
if (msg.silence_reason) node.silence_reason = msg.silence_reason;
|
|
96
|
+
if (children.length > 0) node.children = children;
|
|
97
|
+
|
|
98
|
+
return node;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function ffaContextToYAML(
|
|
102
|
+
messages: RoomMessage[],
|
|
103
|
+
speakerMap: Map<string, string>
|
|
104
|
+
): string {
|
|
105
|
+
const header = [
|
|
106
|
+
"# context_status: default | always | never",
|
|
107
|
+
"# _delete: true — removes this message and all its descendants",
|
|
108
|
+
].join("\n");
|
|
109
|
+
|
|
110
|
+
const rootMsg = messages.find((m) => m.parent_id === null);
|
|
111
|
+
if (!rootMsg) return header + "\n[]";
|
|
66
112
|
|
|
113
|
+
return header + "\n" + YAML.stringify([buildNode(rootMsg, messages, speakerMap, true)], { lineWidth: 0 });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface FfaContextYAMLResult {
|
|
117
|
+
messages: Array<{ id: string; context_status: ContextStatus }>;
|
|
118
|
+
deletedMessageIds: string[];
|
|
119
|
+
implicitDeleteCount: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function ffaContextFromYAML(yamlContent: string): FfaContextYAMLResult {
|
|
123
|
+
const data = YAML.parse(yamlContent) as FfaEditableNode[];
|
|
124
|
+
|
|
125
|
+
const deletedMessageIds: string[] = [];
|
|
126
|
+
const messages: Array<{ id: string; context_status: ContextStatus }> = [];
|
|
127
|
+
let implicitDeleteCount = 0;
|
|
128
|
+
|
|
129
|
+
function collectDeleted(node: FfaEditableNode, parentDeleted: boolean): void {
|
|
130
|
+
const selfDeleted = parentDeleted || !!node._delete;
|
|
131
|
+
if (selfDeleted) {
|
|
132
|
+
deletedMessageIds.push(node.id);
|
|
133
|
+
if (!node._delete) implicitDeleteCount++;
|
|
134
|
+
} else {
|
|
135
|
+
const normalized = (node.context_status ?? 'default').toString().toLowerCase() as ContextStatus;
|
|
136
|
+
messages.push({ id: node.id, context_status: normalized });
|
|
137
|
+
}
|
|
138
|
+
for (const child of node.children ?? []) {
|
|
139
|
+
collectDeleted(child, selfDeleted);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const nodes = data ?? [];
|
|
144
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
145
|
+
const node = nodes[i];
|
|
146
|
+
const isRoot = i === 0;
|
|
147
|
+
if (isRoot && node._delete) continue; // root deletion not allowed — it anchors the room
|
|
148
|
+
collectDeleted(node, false);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { messages, deletedMessageIds, implicitDeleteCount };
|
|
152
|
+
}
|