ei-tui 0.4.3 → 0.5.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/README.md +14 -0
- package/package.json +1 -1
- package/src/cli/README.md +17 -12
- package/src/cli/commands/personas.ts +12 -0
- package/src/cli/mcp.ts +2 -2
- package/src/cli/retrieval.ts +86 -8
- package/src/cli.ts +8 -5
- package/src/core/constants/seed-traits.ts +29 -0
- package/src/core/context-utils.ts +1 -0
- package/src/core/handlers/human-matching.ts +86 -56
- package/src/core/handlers/index.ts +5 -0
- package/src/core/handlers/persona-preview.ts +7 -0
- package/src/core/handlers/persona-topics.ts +3 -2
- package/src/core/handlers/rooms.ts +176 -0
- package/src/core/handlers/utils.ts +55 -3
- package/src/core/heartbeat-manager.ts +3 -1
- package/src/core/llm-client.ts +1 -1
- package/src/core/message-manager.ts +10 -8
- package/src/core/orchestrators/human-extraction.ts +15 -2
- package/src/core/orchestrators/index.ts +1 -0
- package/src/core/orchestrators/persona-generation.ts +4 -0
- package/src/core/orchestrators/persona-topics.ts +2 -1
- package/src/core/orchestrators/room-extraction.ts +318 -0
- package/src/core/persona-manager.ts +16 -5
- package/src/core/personas/opencode-agent.ts +12 -2
- package/src/core/processor.ts +520 -4
- package/src/core/prompt-context-builder.ts +89 -5
- package/src/core/queue-processor.ts +68 -8
- package/src/core/room-manager.ts +408 -0
- package/src/core/state/index.ts +1 -0
- package/src/core/state/personas.ts +12 -2
- package/src/core/state/queue.ts +2 -2
- package/src/core/state/rooms.ts +182 -0
- package/src/core/state-manager.ts +124 -2
- package/src/core/tool-manager.ts +1 -1
- package/src/core/tools/index.ts +15 -0
- package/src/core/types/data-items.ts +3 -1
- package/src/core/types/enums.ts +11 -0
- package/src/core/types/integrations.ts +10 -2
- package/src/core/types/llm.ts +3 -0
- package/src/core/types/rooms.ts +59 -0
- package/src/core/types.ts +1 -0
- package/src/core/utils/decay.ts +14 -8
- package/src/core/utils/exposure.ts +14 -0
- package/src/integrations/claude-code/importer.ts +23 -10
- package/src/integrations/cursor/importer.ts +22 -10
- package/src/integrations/opencode/importer.ts +30 -13
- package/src/prompts/ceremony/dedup.ts +2 -2
- package/src/prompts/generation/from-person.ts +85 -0
- package/src/prompts/generation/index.ts +2 -0
- package/src/prompts/generation/persona.ts +14 -10
- package/src/prompts/generation/seeds.ts +4 -29
- package/src/prompts/generation/types.ts +13 -0
- package/src/prompts/heartbeat/check.ts +1 -1
- package/src/prompts/heartbeat/ei.ts +4 -4
- package/src/prompts/heartbeat/types.ts +1 -0
- package/src/prompts/index.ts +15 -0
- package/src/prompts/message-utils.ts +2 -2
- package/src/prompts/persona/topics-match.ts +7 -6
- package/src/prompts/persona/topics-update.ts +8 -11
- package/src/prompts/persona/types.ts +2 -1
- package/src/prompts/response/index.ts +1 -1
- package/src/prompts/response/sections.ts +20 -8
- package/src/prompts/response/types.ts +6 -0
- package/src/prompts/room/index.ts +115 -0
- package/src/prompts/room/sections.ts +150 -0
- package/src/prompts/room/types.ts +93 -0
- package/tui/README.md +20 -0
- package/tui/src/app.tsx +3 -2
- package/tui/src/commands/activate.tsx +98 -0
- package/tui/src/commands/archive.tsx +54 -25
- package/tui/src/commands/capture.tsx +50 -0
- package/tui/src/commands/dedupe.tsx +2 -7
- package/tui/src/commands/delete.tsx +48 -0
- package/tui/src/commands/details.tsx +7 -0
- package/tui/src/commands/persona.tsx +271 -9
- package/tui/src/commands/room.tsx +261 -0
- package/tui/src/commands/silence.tsx +29 -0
- package/tui/src/components/ArchivedItemsOverlay.tsx +144 -0
- package/tui/src/components/ConfirmOverlay.tsx +6 -0
- package/tui/src/components/ConflictOverlay.tsx +6 -0
- package/tui/src/components/HelpOverlay.tsx +6 -1
- package/tui/src/components/LoadingOverlay.tsx +51 -0
- package/tui/src/components/MessageList.tsx +1 -18
- package/tui/src/components/PersonPickerOverlay.tsx +121 -0
- package/tui/src/components/PersonaListOverlay.tsx +6 -1
- package/tui/src/components/PromptInput.tsx +141 -8
- package/tui/src/components/ProviderListOverlay.tsx +5 -1
- package/tui/src/components/QuotesOverlay.tsx +5 -1
- package/tui/src/components/RoomMessageList.tsx +179 -0
- package/tui/src/components/Sidebar.tsx +54 -2
- package/tui/src/components/StatusBar.tsx +99 -8
- package/tui/src/components/ToolkitListOverlay.tsx +5 -1
- package/tui/src/components/WelcomeOverlay.tsx +6 -0
- package/tui/src/context/ei.tsx +252 -1
- package/tui/src/context/keyboard.tsx +48 -12
- package/tui/src/util/cyp-editor.tsx +152 -0
- package/tui/src/util/quote-utils.ts +19 -0
- package/tui/src/util/room-editor.tsx +164 -0
- package/tui/src/util/room-logic.ts +8 -0
- package/tui/src/util/room-parser.ts +70 -0
- package/tui/src/util/yaml-serializers.ts +151 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useKeyboard } from "@opentui/solid";
|
|
2
|
-
import { For, createSignal, createMemo } from "solid-js";
|
|
2
|
+
import { For, createSignal, createMemo, onMount, onCleanup } from "solid-js";
|
|
3
3
|
import type { KeyEvent } from "@opentui/core";
|
|
4
4
|
import type { PersonaSummary } from "../../../src/core/types.js";
|
|
5
|
+
import { useKeyboardNav } from "../context/keyboard.js";
|
|
5
6
|
|
|
6
7
|
interface PersonaListOverlayProps {
|
|
7
8
|
personas: PersonaSummary[];
|
|
@@ -12,10 +13,14 @@ interface PersonaListOverlayProps {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export function PersonaListOverlay(props: PersonaListOverlayProps) {
|
|
16
|
+
const { setOverlayActive } = useKeyboardNav();
|
|
15
17
|
const [selectedIndex, setSelectedIndex] = createSignal(0);
|
|
16
18
|
const [filterText, setFilterText] = createSignal("");
|
|
17
19
|
const [filterMode, setFilterMode] = createSignal(false);
|
|
18
20
|
|
|
21
|
+
onMount(() => setOverlayActive(true));
|
|
22
|
+
onCleanup(() => setOverlayActive(false));
|
|
23
|
+
|
|
19
24
|
const filteredPersonas = createMemo(() => {
|
|
20
25
|
const filter = filterText().toLowerCase();
|
|
21
26
|
if (!filter) return props.personas;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createEffect, createSignal } from "solid-js";
|
|
1
|
+
import { createEffect, createMemo, createSignal } from "solid-js";
|
|
2
2
|
import { getAllCommands } from "../commands/registry";
|
|
3
3
|
import type { TextareaRenderable, KeyBinding } from "@opentui/core";
|
|
4
4
|
import { useEi } from "../context/ei";
|
|
@@ -26,10 +26,16 @@ import { dlqCommand } from "../commands/dlq";
|
|
|
26
26
|
import { toolsCommand } from "../commands/tools";
|
|
27
27
|
import { authCommand } from '../commands/auth';
|
|
28
28
|
import { dedupeCommand } from "../commands/dedupe";
|
|
29
|
+
import { roomCommand } from "../commands/room.js";
|
|
30
|
+
import { activateCommand } from "../commands/activate.js";
|
|
31
|
+
import { silenceCommand } from "../commands/silence.js";
|
|
32
|
+
import { captureCommand } from "../commands/capture.js";
|
|
33
|
+
import { openCYPEditor } from "../util/cyp-editor.js";
|
|
29
34
|
import { useOverlay } from "../context/overlay";
|
|
30
35
|
import { CommandSuggest } from "./CommandSuggest";
|
|
31
36
|
import { useKeyboard } from "@opentui/solid";
|
|
32
37
|
import type { KeyEvent } from "@opentui/core";
|
|
38
|
+
import { RoomMode } from "../../../src/core/types/enums.js";
|
|
33
39
|
|
|
34
40
|
const TEXTAREA_KEYBINDINGS: KeyBinding[] = [
|
|
35
41
|
{ name: "return", action: "submit" },
|
|
@@ -39,7 +45,23 @@ const TEXTAREA_KEYBINDINGS: KeyBinding[] = [
|
|
|
39
45
|
|
|
40
46
|
export function PromptInput() {
|
|
41
47
|
const ei = useEi();
|
|
42
|
-
const {
|
|
48
|
+
const {
|
|
49
|
+
sendMessage,
|
|
50
|
+
activePersonaId,
|
|
51
|
+
stopProcessor,
|
|
52
|
+
showNotification,
|
|
53
|
+
activeRoomId,
|
|
54
|
+
getRoom,
|
|
55
|
+
roomMessages,
|
|
56
|
+
roomActivePath,
|
|
57
|
+
personas,
|
|
58
|
+
sendFfaMessage,
|
|
59
|
+
submitHumanRoomMessage,
|
|
60
|
+
recallHumanRoomMessage,
|
|
61
|
+
activateRoom,
|
|
62
|
+
selectCYPBranch,
|
|
63
|
+
humanRoomMessagePending,
|
|
64
|
+
} = ei;
|
|
43
65
|
const { registerTextarea, registerEditorHandler, exitApp, renderer, resetHistoryIndex } = useKeyboardNav();
|
|
44
66
|
const { showOverlay, hideOverlay, overlayRenderer } = useOverlay();
|
|
45
67
|
|
|
@@ -49,12 +71,9 @@ export function PromptInput() {
|
|
|
49
71
|
registerCommand(quotesCommand);
|
|
50
72
|
registerCommand(editorCommand);
|
|
51
73
|
registerCommand(personaCommand);
|
|
74
|
+
registerCommand(roomCommand);
|
|
52
75
|
registerCommand(detailsCommand);
|
|
53
|
-
registerCommand(archiveCommand);
|
|
54
|
-
registerCommand(unarchiveCommand);
|
|
55
76
|
registerCommand(newCommand);
|
|
56
|
-
registerCommand(pauseCommand);
|
|
57
|
-
registerCommand(resumeCommand);
|
|
58
77
|
registerCommand(modelCommand);
|
|
59
78
|
registerCommand(settingsCommand);
|
|
60
79
|
registerCommand(providerCommand);
|
|
@@ -64,14 +83,36 @@ export function PromptInput() {
|
|
|
64
83
|
registerCommand(queueCommand);
|
|
65
84
|
registerCommand(dlqCommand);
|
|
66
85
|
registerCommand(toolsCommand);
|
|
67
|
-
registerCommand(authCommand);
|
|
68
86
|
registerCommand(dedupeCommand);
|
|
87
|
+
registerCommand(activateCommand);
|
|
88
|
+
registerCommand(silenceCommand);
|
|
89
|
+
registerCommand(captureCommand);
|
|
90
|
+
registerCommand(authCommand);
|
|
91
|
+
registerCommand(pauseCommand);
|
|
92
|
+
registerCommand(resumeCommand);
|
|
93
|
+
registerCommand(archiveCommand);
|
|
94
|
+
registerCommand(unarchiveCommand);
|
|
69
95
|
|
|
70
96
|
let textareaRef: TextareaRenderable | undefined;
|
|
71
97
|
|
|
72
98
|
const [inputText, setInputText] = createSignal("");
|
|
73
99
|
const [suggestIndex, setSuggestIndex] = createSignal(0);
|
|
74
100
|
|
|
101
|
+
const allPersonasResponded = createMemo(() => {
|
|
102
|
+
const roomId = activeRoomId();
|
|
103
|
+
if (!roomId) return false;
|
|
104
|
+
const room = getRoom(roomId);
|
|
105
|
+
if (!room?.active_node_id) return false;
|
|
106
|
+
const respondedIds = new Set(
|
|
107
|
+
roomMessages()
|
|
108
|
+
.filter(m => m.parent_id === room.active_node_id && m.role === "persona" && m.persona_id)
|
|
109
|
+
.map(m => m.persona_id!)
|
|
110
|
+
);
|
|
111
|
+
const judgeId = room.judge_persona_id;
|
|
112
|
+
const nonJudgeIds = room.persona_ids.filter(id => id !== judgeId);
|
|
113
|
+
return nonJudgeIds.every(id => respondedIds.has(id));
|
|
114
|
+
});
|
|
115
|
+
|
|
75
116
|
const suggestMatches = () => {
|
|
76
117
|
const raw = inputText().trim();
|
|
77
118
|
if (!raw.startsWith("/")) return [];
|
|
@@ -95,7 +136,46 @@ export function PromptInput() {
|
|
|
95
136
|
resetHistoryIndex();
|
|
96
137
|
});
|
|
97
138
|
|
|
139
|
+
createEffect(() => {
|
|
140
|
+
if (activeRoomId() && !humanRoomMessagePending()) {
|
|
141
|
+
const room = getRoom(activeRoomId()!);
|
|
142
|
+
if (room?.mode !== RoomMode.FreeForAll && allPersonasResponded() && !humanRoomMessagePending()) {
|
|
143
|
+
showNotification("Use /silence to pass", "info");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
98
148
|
useKeyboard((event: KeyEvent) => {
|
|
149
|
+
if (event.name === "up" && activeRoomId() && humanRoomMessagePending()) {
|
|
150
|
+
const room = getRoom(activeRoomId()!);
|
|
151
|
+
if (room?.mode !== RoomMode.FreeForAll) {
|
|
152
|
+
// Lock check: if any child of active_node has children, the node is explored — don't allow recall
|
|
153
|
+
const activeNodeId = room?.active_node_id;
|
|
154
|
+
const allMessages = roomMessages();
|
|
155
|
+
const childrenOfActiveNode = allMessages.filter(m => m.parent_id === activeNodeId);
|
|
156
|
+
const isLocked = childrenOfActiveNode.some(child =>
|
|
157
|
+
allMessages.some(m => m.parent_id === child.id)
|
|
158
|
+
);
|
|
159
|
+
if (isLocked) {
|
|
160
|
+
showNotification("Cannot recall — this path has already been explored", "warn");
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const pendingMsg = allMessages.find(
|
|
165
|
+
m => m.parent_id === room?.active_node_id && m.role === "human"
|
|
166
|
+
);
|
|
167
|
+
const recalled = recallHumanRoomMessage();
|
|
168
|
+
if (recalled) {
|
|
169
|
+
const content = pendingMsg?.verbal_response ?? pendingMsg?.silence_reason ?? "";
|
|
170
|
+
textareaRef?.setText(content);
|
|
171
|
+
setInputText(content);
|
|
172
|
+
textareaRef?.gotoBufferEnd();
|
|
173
|
+
}
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
99
179
|
if (!suggestVisible()) return;
|
|
100
180
|
|
|
101
181
|
if (event.name === "up") {
|
|
@@ -144,7 +224,32 @@ export function PromptInput() {
|
|
|
144
224
|
});
|
|
145
225
|
|
|
146
226
|
const handleSubmit = async () => {
|
|
147
|
-
const text = textareaRef?.plainText?.trim();
|
|
227
|
+
const text = textareaRef?.plainText?.trim() ?? "";
|
|
228
|
+
|
|
229
|
+
if (activeRoomId()) {
|
|
230
|
+
const room = getRoom(activeRoomId()!);
|
|
231
|
+
|
|
232
|
+
if (room?.mode !== RoomMode.FreeForAll && !text) {
|
|
233
|
+
if (humanRoomMessagePending() && allPersonasResponded()) {
|
|
234
|
+
if (room?.mode === RoomMode.ChooseYourPath && room.active_node_id) {
|
|
235
|
+
await openCYPEditor({
|
|
236
|
+
roomId: activeRoomId()!,
|
|
237
|
+
activeNodeId: room.active_node_id,
|
|
238
|
+
messages: roomMessages(),
|
|
239
|
+
activePath: roomActivePath(),
|
|
240
|
+
personas: personas(),
|
|
241
|
+
selectBranch: selectCYPBranch,
|
|
242
|
+
showNotification,
|
|
243
|
+
renderer,
|
|
244
|
+
});
|
|
245
|
+
} else {
|
|
246
|
+
await activateRoom();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
148
253
|
if (!text) return;
|
|
149
254
|
|
|
150
255
|
if (text.startsWith("/")) {
|
|
@@ -181,6 +286,33 @@ export function PromptInput() {
|
|
|
181
286
|
return;
|
|
182
287
|
}
|
|
183
288
|
|
|
289
|
+
if (activeRoomId()) {
|
|
290
|
+
const room = getRoom(activeRoomId()!);
|
|
291
|
+
if (room?.mode === RoomMode.FreeForAll) {
|
|
292
|
+
textareaRef?.clear();
|
|
293
|
+
setInputText("");
|
|
294
|
+
await sendFfaMessage(text, undefined);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!humanRoomMessagePending()) {
|
|
299
|
+
const msgId = submitHumanRoomMessage(text, undefined);
|
|
300
|
+
if (msgId !== null) {
|
|
301
|
+
textareaRef?.clear();
|
|
302
|
+
setInputText("");
|
|
303
|
+
}
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (humanRoomMessagePending() && allPersonasResponded()) {
|
|
308
|
+
await activateRoom();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
showNotification("Waiting for participants to respond...", "info");
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
184
316
|
textareaRef?.clear();
|
|
185
317
|
setInputText("");
|
|
186
318
|
resetHistoryIndex();
|
|
@@ -196,6 +328,7 @@ export function PromptInput() {
|
|
|
196
328
|
registerEditorHandler(handleEditor);
|
|
197
329
|
|
|
198
330
|
const getPlaceholder = () => {
|
|
331
|
+
if (activeRoomId() && humanRoomMessagePending()) return "Response Submitted - Press [Up] to recall";
|
|
199
332
|
if (!activePersonaId()) return "Select a persona...";
|
|
200
333
|
return "Type your message... (Enter to send, Ctrl+E for editor)";
|
|
201
334
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useKeyboard } from "@opentui/solid";
|
|
2
|
-
import { For, createSignal, createMemo } from "solid-js";
|
|
2
|
+
import { For, createSignal, createMemo, onMount, onCleanup } from "solid-js";
|
|
3
3
|
import type { KeyEvent } from "@opentui/core";
|
|
4
|
+
import { useKeyboardNav } from "../context/keyboard.js";
|
|
4
5
|
|
|
5
6
|
export interface ProviderListItem {
|
|
6
7
|
id: string;
|
|
@@ -20,6 +21,9 @@ interface ProviderListOverlayProps {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export function ProviderListOverlay(props: ProviderListOverlayProps) {
|
|
24
|
+
const { setOverlayActive } = useKeyboardNav();
|
|
25
|
+
onMount(() => setOverlayActive(true));
|
|
26
|
+
onCleanup(() => setOverlayActive(false));
|
|
23
27
|
const [selectedIndex, setSelectedIndex] = createSignal(0);
|
|
24
28
|
const [filterText, setFilterText] = createSignal("");
|
|
25
29
|
const [filterMode, setFilterMode] = createSignal(false);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useKeyboard } from "@opentui/solid";
|
|
2
|
-
import { For, createSignal, createMemo } from "solid-js";
|
|
2
|
+
import { For, createSignal, createMemo, onMount, onCleanup } from "solid-js";
|
|
3
3
|
import type { KeyEvent } from "@opentui/core";
|
|
4
4
|
import type { Quote } from "../../../src/core/types.js";
|
|
5
|
+
import { useKeyboardNav } from "../context/keyboard.js";
|
|
5
6
|
|
|
6
7
|
interface QuotesOverlayProps {
|
|
7
8
|
quotes: Quote[];
|
|
@@ -12,6 +13,9 @@ interface QuotesOverlayProps {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export function QuotesOverlay(props: QuotesOverlayProps) {
|
|
16
|
+
const { setOverlayActive } = useKeyboardNav();
|
|
17
|
+
onMount(() => setOverlayActive(true));
|
|
18
|
+
onCleanup(() => setOverlayActive(false));
|
|
15
19
|
const [selectedIndex, setSelectedIndex] = createSignal(0);
|
|
16
20
|
const [confirmDelete, setConfirmDelete] = createSignal(false);
|
|
17
21
|
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { For, Show, createMemo, createSignal, createEffect, on, onCleanup } from "solid-js";
|
|
2
|
+
import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core";
|
|
3
|
+
import { useEi } from "../context/ei.js";
|
|
4
|
+
import { useKeyboardNav } from "../context/keyboard.js";
|
|
5
|
+
import { solarizedDarkSyntax } from "../util/syntax.js";
|
|
6
|
+
import type { RoomMessage, Quote } from "../../../src/core/types.js";
|
|
7
|
+
import { RoomMode } from "../../../src/core/types/enums.js";
|
|
8
|
+
import { insertQuoteMarkers } from "../util/quote-utils.js";
|
|
9
|
+
|
|
10
|
+
interface RoomMessageWithQuotes extends RoomMessage {
|
|
11
|
+
_quotes: Quote[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function formatTime(timestamp: string): string {
|
|
15
|
+
const date = new Date(timestamp);
|
|
16
|
+
const hours = date.getHours().toString().padStart(2, "0");
|
|
17
|
+
const minutes = date.getMinutes().toString().padStart(2, "0");
|
|
18
|
+
return `${hours}:${minutes}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function RoomMessageList() {
|
|
22
|
+
const { roomMessages, roomActivePath, personas, activeRoomId, getRoom, getQuotes, quotesVersion } = useEi();
|
|
23
|
+
const { registerMessageScroll } = useKeyboardNav();
|
|
24
|
+
|
|
25
|
+
const personaNameMap = createMemo(() => {
|
|
26
|
+
const map = new Map<string, string>();
|
|
27
|
+
for (const p of personas()) {
|
|
28
|
+
map.set(p.id, p.display_name);
|
|
29
|
+
}
|
|
30
|
+
return map;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const messageIndices = createMemo(() => {
|
|
34
|
+
const all = roomMessages();
|
|
35
|
+
const indexMap = new Map<string, number>();
|
|
36
|
+
all.forEach((m, i) => indexMap.set(m.id, i + 1));
|
|
37
|
+
return indexMap;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const siblingCounts = createMemo(() => {
|
|
41
|
+
const all = roomMessages();
|
|
42
|
+
const countMap = new Map<string, number>();
|
|
43
|
+
for (const msg of all) {
|
|
44
|
+
const siblings = all.filter(m => m.parent_id === msg.parent_id && m.id !== msg.id && m.parent_id !== null);
|
|
45
|
+
countMap.set(msg.id, siblings.length);
|
|
46
|
+
}
|
|
47
|
+
return countMap;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const activeRoom = createMemo(() => {
|
|
51
|
+
const id = activeRoomId();
|
|
52
|
+
return id ? getRoom(id) : null;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const displayMessages = createMemo(() => {
|
|
56
|
+
if (activeRoom()?.mode === RoomMode.ChooseYourPath) {
|
|
57
|
+
return roomActivePath();
|
|
58
|
+
}
|
|
59
|
+
if (activeRoom()?.mode === RoomMode.MessagesAgainstPersona) {
|
|
60
|
+
const activeNodeId = activeRoom()?.active_node_id;
|
|
61
|
+
if (!activeNodeId) return roomMessages();
|
|
62
|
+
return roomMessages().filter(m => m.parent_id !== activeNodeId);
|
|
63
|
+
}
|
|
64
|
+
return roomMessages();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const [allQuotes, setAllQuotes] = createSignal<Quote[]>([]);
|
|
68
|
+
|
|
69
|
+
createEffect(on(() => [roomMessages(), quotesVersion()], () => {
|
|
70
|
+
void getQuotes().then(setAllQuotes);
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
const quotesByMessage = createMemo(() => {
|
|
74
|
+
const map = new Map<string, Quote[]>();
|
|
75
|
+
for (const quote of allQuotes()) {
|
|
76
|
+
if (quote.message_id) {
|
|
77
|
+
const existing = map.get(quote.message_id) ?? [];
|
|
78
|
+
existing.push(quote);
|
|
79
|
+
map.set(quote.message_id, existing);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return map;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const displayMessagesWithQuotes = createMemo<RoomMessageWithQuotes[]>(() => {
|
|
86
|
+
const qMap = quotesByMessage();
|
|
87
|
+
return displayMessages().map(msg => ({
|
|
88
|
+
...msg,
|
|
89
|
+
_quotes: qMap.get(msg.id) ?? [],
|
|
90
|
+
}));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const getSpeakerName = (msg: RoomMessage): string => {
|
|
94
|
+
if (msg.role === "human") return "Human";
|
|
95
|
+
if (msg.persona_id) return personaNameMap().get(msg.persona_id) ?? msg.persona_id;
|
|
96
|
+
return "Persona";
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const getSpeakerColor = (msg: RoomMessage): string => {
|
|
100
|
+
if (msg.role === "human") return "#268bd2";
|
|
101
|
+
return "#b58900";
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const handleScrollboxRef = (scrollbox: ScrollBoxRenderable) => {
|
|
105
|
+
registerMessageScroll(scrollbox);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
onCleanup(() => {
|
|
109
|
+
registerMessageScroll(null as unknown as ScrollBoxRenderable);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<box flexGrow={1}>
|
|
114
|
+
<Show
|
|
115
|
+
when={displayMessages().length > 0}
|
|
116
|
+
fallback={
|
|
117
|
+
<box flexGrow={1} padding={1} backgroundColor="#0f1419" justifyContent="center" alignItems="center">
|
|
118
|
+
<text fg="#586e75" content="No messages yet." />
|
|
119
|
+
</box>
|
|
120
|
+
}
|
|
121
|
+
>
|
|
122
|
+
<scrollbox
|
|
123
|
+
ref={handleScrollboxRef}
|
|
124
|
+
flexGrow={1}
|
|
125
|
+
padding={1}
|
|
126
|
+
backgroundColor="#0f1419"
|
|
127
|
+
stickyScroll={true}
|
|
128
|
+
stickyStart="bottom"
|
|
129
|
+
>
|
|
130
|
+
<For each={displayMessagesWithQuotes()}>
|
|
131
|
+
{(msg) => {
|
|
132
|
+
const speakerName = getSpeakerName(msg);
|
|
133
|
+
const speakerColor = getSpeakerColor(msg);
|
|
134
|
+
const idx = messageIndices().get(msg.id) ?? "?";
|
|
135
|
+
const siblingCount = siblingCounts().get(msg.id) ?? 0;
|
|
136
|
+
const branchIndicator = (siblingCount > 0 && activeRoom()?.mode === RoomMode.ChooseYourPath)
|
|
137
|
+
? ` ⑂${siblingCount}`
|
|
138
|
+
: "";
|
|
139
|
+
const header = `${speakerName} (${formatTime(msg.timestamp)}) [${idx}]${branchIndicator}:`;
|
|
140
|
+
const isSilence = msg.silence_reason !== undefined && !msg.verbal_response;
|
|
141
|
+
const isJudge = activeRoom()?.judge_persona_id !== undefined
|
|
142
|
+
&& msg.persona_id === activeRoom()?.judge_persona_id;
|
|
143
|
+
const silenceText = isSilence
|
|
144
|
+
? isJudge
|
|
145
|
+
? `[${speakerName}'s verdict: ${msg.silence_reason ?? ""}]`
|
|
146
|
+
: `[${speakerName} chose not to respond: ${msg.silence_reason ?? ""}]`
|
|
147
|
+
: "";
|
|
148
|
+
const contentParts: string[] = [];
|
|
149
|
+
if (msg.action_response) contentParts.push(`_${msg.action_response}_`);
|
|
150
|
+
if (msg.verbal_response) contentParts.push(msg.verbal_response);
|
|
151
|
+
const msgQuotes = msg._quotes;
|
|
152
|
+
const normalContent = insertQuoteMarkers(contentParts.join("\n\n"), msgQuotes);
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<box flexDirection="column" marginBottom={1}>
|
|
156
|
+
<text
|
|
157
|
+
fg={speakerColor}
|
|
158
|
+
attributes={TextAttributes.BOLD}
|
|
159
|
+
content={header}
|
|
160
|
+
/>
|
|
161
|
+
<box marginLeft={2} visible={isSilence}>
|
|
162
|
+
<text fg="#586e75" content={silenceText} />
|
|
163
|
+
</box>
|
|
164
|
+
<box marginLeft={2} visible={!isSilence}>
|
|
165
|
+
<markdown
|
|
166
|
+
content={normalContent}
|
|
167
|
+
syntaxStyle={solarizedDarkSyntax}
|
|
168
|
+
conceal={true}
|
|
169
|
+
/>
|
|
170
|
+
</box>
|
|
171
|
+
</box>
|
|
172
|
+
);
|
|
173
|
+
}}
|
|
174
|
+
</For>
|
|
175
|
+
</scrollbox>
|
|
176
|
+
</Show>
|
|
177
|
+
</box>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -1,18 +1,37 @@
|
|
|
1
1
|
import { For, createSignal, createEffect, createMemo, onCleanup } from "solid-js";
|
|
2
2
|
import { useEi } from "../context/ei";
|
|
3
3
|
import { useKeyboardNav } from "../context/keyboard";
|
|
4
|
+
import { RoomMode } from "../../../src/core/types/enums.js";
|
|
5
|
+
import type { RoomSummary } from "../../../src/core/types.js";
|
|
6
|
+
|
|
7
|
+
const modeBadge = (mode: RoomMode): string => {
|
|
8
|
+
switch (mode) {
|
|
9
|
+
case RoomMode.ChooseYourPath: return "[CYP]";
|
|
10
|
+
case RoomMode.FreeForAll: return "[FFA]";
|
|
11
|
+
case RoomMode.MessagesAgainstPersona: return "[MAP]";
|
|
12
|
+
default: return "";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
4
15
|
|
|
5
16
|
export function Sidebar() {
|
|
6
|
-
const { personas, activePersonaId } = useEi();
|
|
17
|
+
const { personas, activePersonaId, rooms, activeRoomId } = useEi();
|
|
7
18
|
const { focusedPanel } = useKeyboardNav();
|
|
8
19
|
|
|
9
20
|
const isFocused = () => focusedPanel() === "sidebar";
|
|
21
|
+
const isRoomMode = () => activeRoomId() !== null;
|
|
10
22
|
|
|
11
23
|
// Memoize visible (non-archived) personas for proper reactivity
|
|
12
24
|
const visiblePersonas = createMemo(() =>
|
|
13
25
|
personas().filter(p => !p.is_archived)
|
|
14
26
|
);
|
|
15
27
|
|
|
28
|
+
// Memoize visible (non-archived) rooms sorted by last_activity desc
|
|
29
|
+
const visibleRooms = createMemo(() =>
|
|
30
|
+
rooms()
|
|
31
|
+
.filter((r: RoomSummary) => !r.is_archived)
|
|
32
|
+
.sort((a: RoomSummary, b: RoomSummary) => b.last_activity.localeCompare(a.last_activity))
|
|
33
|
+
);
|
|
34
|
+
|
|
16
35
|
const [highlightedPersona, setHighlightedPersona] = createSignal<string | null>(null);
|
|
17
36
|
let highlightTimer: ReturnType<typeof setTimeout> | null = null;
|
|
18
37
|
|
|
@@ -45,7 +64,7 @@ export function Sidebar() {
|
|
|
45
64
|
>
|
|
46
65
|
<box flexDirection="column">
|
|
47
66
|
<text fg={isFocused() ? "#268bd2" : "#93a1a1"} marginBottom={1}>
|
|
48
|
-
{
|
|
67
|
+
{isRoomMode() ? `/p Personas | * Rooms` : `* Personas | /r Rooms`}
|
|
49
68
|
</text>
|
|
50
69
|
|
|
51
70
|
<scrollbox height="100%">
|
|
@@ -71,6 +90,7 @@ export function Sidebar() {
|
|
|
71
90
|
|
|
72
91
|
return (
|
|
73
92
|
<box
|
|
93
|
+
visible={!isRoomMode()}
|
|
74
94
|
flexDirection="column"
|
|
75
95
|
backgroundColor={
|
|
76
96
|
isActive() && highlightedPersona() === persona.id
|
|
@@ -92,6 +112,38 @@ export function Sidebar() {
|
|
|
92
112
|
);
|
|
93
113
|
}}
|
|
94
114
|
</For>
|
|
115
|
+
<For each={visibleRooms()}>
|
|
116
|
+
{(room) => {
|
|
117
|
+
const isActive = () => activeRoomId() === room.id;
|
|
118
|
+
|
|
119
|
+
const getLabel = () => {
|
|
120
|
+
const prefix = isActive() ? "* " : " ";
|
|
121
|
+
const name = room.display_name;
|
|
122
|
+
const badge = modeBadge(room.mode);
|
|
123
|
+
const unread = room.unread_count > 0 ? ` (${room.unread_count} new)` : "";
|
|
124
|
+
return `${prefix}${name} ${badge}${unread}`;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const textColor = () => {
|
|
128
|
+
if (isActive()) return "#eee8d5";
|
|
129
|
+
return "#839496";
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<box
|
|
134
|
+
visible={isRoomMode()}
|
|
135
|
+
flexDirection="column"
|
|
136
|
+
backgroundColor={isActive() ? "#2d3748" : "transparent"}
|
|
137
|
+
paddingX={1}
|
|
138
|
+
marginBottom={1}
|
|
139
|
+
>
|
|
140
|
+
<text fg={textColor()}>
|
|
141
|
+
{getLabel()}
|
|
142
|
+
</text>
|
|
143
|
+
</box>
|
|
144
|
+
);
|
|
145
|
+
}}
|
|
146
|
+
</For>
|
|
95
147
|
</scrollbox>
|
|
96
148
|
</box>
|
|
97
149
|
</box>
|
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
import { Show } from "solid-js";
|
|
1
|
+
import { Show, createMemo } from "solid-js";
|
|
2
2
|
import { useEi } from "../context/ei";
|
|
3
3
|
import { useKeyboardNav } from "../context/keyboard";
|
|
4
|
+
import { RoomMode } from "../../../src/core/types/enums.js";
|
|
4
5
|
|
|
5
6
|
export function StatusBar() {
|
|
6
|
-
const {
|
|
7
|
+
const {
|
|
8
|
+
activePersonaId,
|
|
9
|
+
personas,
|
|
10
|
+
queueStatus,
|
|
11
|
+
notification,
|
|
12
|
+
activeRoomId,
|
|
13
|
+
roomMessages,
|
|
14
|
+
getRoom,
|
|
15
|
+
rooms,
|
|
16
|
+
humanRoomMessagePending,
|
|
17
|
+
isRoomProcessing,
|
|
18
|
+
} = useEi();
|
|
7
19
|
const { focusedPanel, sidebarVisible } = useKeyboardNav();
|
|
8
20
|
|
|
9
21
|
const getActiveDisplayName = () => {
|
|
@@ -44,6 +56,73 @@ export function StatusBar() {
|
|
|
44
56
|
return "#2aa198";
|
|
45
57
|
};
|
|
46
58
|
|
|
59
|
+
const respondedPersonaIds = createMemo(() => {
|
|
60
|
+
const roomId = activeRoomId();
|
|
61
|
+
if (!roomId) return new Set<string>();
|
|
62
|
+
const roomSummary = rooms().find(r => r.id === roomId);
|
|
63
|
+
if (!roomSummary?.active_node_id) return new Set<string>();
|
|
64
|
+
const msgs = roomMessages().filter(
|
|
65
|
+
m => m.parent_id === roomSummary.active_node_id && m.role === "persona" && m.persona_id
|
|
66
|
+
);
|
|
67
|
+
return new Set(msgs.map(m => m.persona_id!));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const allPersonasResponded = createMemo(() => {
|
|
71
|
+
const roomId = activeRoomId();
|
|
72
|
+
if (!roomId) return false;
|
|
73
|
+
const room = getRoom(roomId);
|
|
74
|
+
if (!room) return false;
|
|
75
|
+
const judgeId = room.judge_persona_id;
|
|
76
|
+
const nonJudgeIds = room.persona_ids.filter(id => id !== judgeId);
|
|
77
|
+
return nonJudgeIds.every(id => respondedPersonaIds().has(id));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const pendingPersonaNames = createMemo(() => {
|
|
81
|
+
const roomId = activeRoomId();
|
|
82
|
+
if (!roomId) return [];
|
|
83
|
+
const room = getRoom(roomId);
|
|
84
|
+
if (!room) return [];
|
|
85
|
+
const judgeId = room.judge_persona_id;
|
|
86
|
+
const allPersonas = personas();
|
|
87
|
+
return room.persona_ids
|
|
88
|
+
.filter(id => id !== judgeId && !respondedPersonaIds().has(id))
|
|
89
|
+
.map(id => allPersonas.find(p => p.id === id)?.display_name ?? id);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const getRoomWaitingText = () => {
|
|
93
|
+
const names: string[] = [];
|
|
94
|
+
if (!humanRoomMessagePending()) {
|
|
95
|
+
names.push("You");
|
|
96
|
+
}
|
|
97
|
+
names.push(...pendingPersonaNames());
|
|
98
|
+
return "Waiting for " + names.join(", ") + "...";
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const activeRoom = () => {
|
|
102
|
+
const roomId = activeRoomId();
|
|
103
|
+
if (!roomId) return null;
|
|
104
|
+
return getRoom(roomId);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const centerIndicator = createMemo(() => {
|
|
108
|
+
const roomId = activeRoomId();
|
|
109
|
+
if (roomId) {
|
|
110
|
+
const room = getRoom(roomId);
|
|
111
|
+
if (room?.mode !== RoomMode.FreeForAll) {
|
|
112
|
+
if (isRoomProcessing()) {
|
|
113
|
+
return { text: "[Waiting]", color: "#586e75" };
|
|
114
|
+
}
|
|
115
|
+
if (humanRoomMessagePending() && allPersonasResponded()) {
|
|
116
|
+
return { text: "[Activate!]", color: "#b58900" };
|
|
117
|
+
}
|
|
118
|
+
if (humanRoomMessagePending()) {
|
|
119
|
+
return { text: "[Waiting]", color: "#586e75" };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { text: `[${getFocusIndicator()}]`, color: "#586e75" };
|
|
124
|
+
});
|
|
125
|
+
|
|
47
126
|
return (
|
|
48
127
|
<box
|
|
49
128
|
height={1}
|
|
@@ -54,11 +133,23 @@ export function StatusBar() {
|
|
|
54
133
|
>
|
|
55
134
|
<box flexGrow={1}>
|
|
56
135
|
<Show when={notification()} fallback={
|
|
57
|
-
<
|
|
58
|
-
<
|
|
59
|
-
{getActiveDisplayName()}
|
|
136
|
+
<Show when={activeRoomId()} fallback={
|
|
137
|
+
<text fg="#586e75">
|
|
138
|
+
<Show when={getActiveDisplayName()} fallback="No persona selected">
|
|
139
|
+
{getActiveDisplayName()}
|
|
140
|
+
</Show>
|
|
141
|
+
</text>
|
|
142
|
+
}>
|
|
143
|
+
<Show when={allPersonasResponded() && humanRoomMessagePending()} fallback={
|
|
144
|
+
<text fg="#586e75">
|
|
145
|
+
{getRoomWaitingText()}
|
|
146
|
+
</text>
|
|
147
|
+
}>
|
|
148
|
+
<text fg="#586e75">
|
|
149
|
+
{activeRoom()?.display_name ?? ""}
|
|
150
|
+
</text>
|
|
60
151
|
</Show>
|
|
61
|
-
</
|
|
152
|
+
</Show>
|
|
62
153
|
}>
|
|
63
154
|
<text fg={getNotificationColor()}>
|
|
64
155
|
{notification()?.message}
|
|
@@ -66,8 +157,8 @@ export function StatusBar() {
|
|
|
66
157
|
</Show>
|
|
67
158
|
</box>
|
|
68
159
|
|
|
69
|
-
<text fg=
|
|
70
|
-
|
|
160
|
+
<text fg={centerIndicator().color} marginRight={2}>
|
|
161
|
+
{centerIndicator().text}
|
|
71
162
|
</text>
|
|
72
163
|
|
|
73
164
|
<Show when={!sidebarVisible()}>
|