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.
Files changed (102) hide show
  1. package/README.md +14 -0
  2. package/package.json +1 -1
  3. package/src/cli/README.md +17 -12
  4. package/src/cli/commands/personas.ts +12 -0
  5. package/src/cli/mcp.ts +2 -2
  6. package/src/cli/retrieval.ts +86 -8
  7. package/src/cli.ts +8 -5
  8. package/src/core/constants/seed-traits.ts +29 -0
  9. package/src/core/context-utils.ts +1 -0
  10. package/src/core/handlers/human-matching.ts +86 -56
  11. package/src/core/handlers/index.ts +5 -0
  12. package/src/core/handlers/persona-preview.ts +7 -0
  13. package/src/core/handlers/persona-topics.ts +3 -2
  14. package/src/core/handlers/rooms.ts +176 -0
  15. package/src/core/handlers/utils.ts +55 -3
  16. package/src/core/heartbeat-manager.ts +3 -1
  17. package/src/core/llm-client.ts +1 -1
  18. package/src/core/message-manager.ts +10 -8
  19. package/src/core/orchestrators/human-extraction.ts +15 -2
  20. package/src/core/orchestrators/index.ts +1 -0
  21. package/src/core/orchestrators/persona-generation.ts +4 -0
  22. package/src/core/orchestrators/persona-topics.ts +2 -1
  23. package/src/core/orchestrators/room-extraction.ts +318 -0
  24. package/src/core/persona-manager.ts +16 -5
  25. package/src/core/personas/opencode-agent.ts +12 -2
  26. package/src/core/processor.ts +520 -4
  27. package/src/core/prompt-context-builder.ts +89 -5
  28. package/src/core/queue-processor.ts +68 -8
  29. package/src/core/room-manager.ts +408 -0
  30. package/src/core/state/index.ts +1 -0
  31. package/src/core/state/personas.ts +12 -2
  32. package/src/core/state/queue.ts +2 -2
  33. package/src/core/state/rooms.ts +182 -0
  34. package/src/core/state-manager.ts +124 -2
  35. package/src/core/tool-manager.ts +1 -1
  36. package/src/core/tools/index.ts +15 -0
  37. package/src/core/types/data-items.ts +3 -1
  38. package/src/core/types/enums.ts +11 -0
  39. package/src/core/types/integrations.ts +10 -2
  40. package/src/core/types/llm.ts +3 -0
  41. package/src/core/types/rooms.ts +59 -0
  42. package/src/core/types.ts +1 -0
  43. package/src/core/utils/decay.ts +14 -8
  44. package/src/core/utils/exposure.ts +14 -0
  45. package/src/integrations/claude-code/importer.ts +23 -10
  46. package/src/integrations/cursor/importer.ts +22 -10
  47. package/src/integrations/opencode/importer.ts +30 -13
  48. package/src/prompts/ceremony/dedup.ts +2 -2
  49. package/src/prompts/generation/from-person.ts +85 -0
  50. package/src/prompts/generation/index.ts +2 -0
  51. package/src/prompts/generation/persona.ts +14 -10
  52. package/src/prompts/generation/seeds.ts +4 -29
  53. package/src/prompts/generation/types.ts +13 -0
  54. package/src/prompts/heartbeat/check.ts +1 -1
  55. package/src/prompts/heartbeat/ei.ts +4 -4
  56. package/src/prompts/heartbeat/types.ts +1 -0
  57. package/src/prompts/index.ts +15 -0
  58. package/src/prompts/message-utils.ts +2 -2
  59. package/src/prompts/persona/topics-match.ts +7 -6
  60. package/src/prompts/persona/topics-update.ts +8 -11
  61. package/src/prompts/persona/types.ts +2 -1
  62. package/src/prompts/response/index.ts +1 -1
  63. package/src/prompts/response/sections.ts +20 -8
  64. package/src/prompts/response/types.ts +6 -0
  65. package/src/prompts/room/index.ts +115 -0
  66. package/src/prompts/room/sections.ts +150 -0
  67. package/src/prompts/room/types.ts +93 -0
  68. package/tui/README.md +20 -0
  69. package/tui/src/app.tsx +3 -2
  70. package/tui/src/commands/activate.tsx +98 -0
  71. package/tui/src/commands/archive.tsx +54 -25
  72. package/tui/src/commands/capture.tsx +50 -0
  73. package/tui/src/commands/dedupe.tsx +2 -7
  74. package/tui/src/commands/delete.tsx +48 -0
  75. package/tui/src/commands/details.tsx +7 -0
  76. package/tui/src/commands/persona.tsx +271 -9
  77. package/tui/src/commands/room.tsx +261 -0
  78. package/tui/src/commands/silence.tsx +29 -0
  79. package/tui/src/components/ArchivedItemsOverlay.tsx +144 -0
  80. package/tui/src/components/ConfirmOverlay.tsx +6 -0
  81. package/tui/src/components/ConflictOverlay.tsx +6 -0
  82. package/tui/src/components/HelpOverlay.tsx +6 -1
  83. package/tui/src/components/LoadingOverlay.tsx +51 -0
  84. package/tui/src/components/MessageList.tsx +1 -18
  85. package/tui/src/components/PersonPickerOverlay.tsx +121 -0
  86. package/tui/src/components/PersonaListOverlay.tsx +6 -1
  87. package/tui/src/components/PromptInput.tsx +141 -8
  88. package/tui/src/components/ProviderListOverlay.tsx +5 -1
  89. package/tui/src/components/QuotesOverlay.tsx +5 -1
  90. package/tui/src/components/RoomMessageList.tsx +179 -0
  91. package/tui/src/components/Sidebar.tsx +54 -2
  92. package/tui/src/components/StatusBar.tsx +99 -8
  93. package/tui/src/components/ToolkitListOverlay.tsx +5 -1
  94. package/tui/src/components/WelcomeOverlay.tsx +6 -0
  95. package/tui/src/context/ei.tsx +252 -1
  96. package/tui/src/context/keyboard.tsx +48 -12
  97. package/tui/src/util/cyp-editor.tsx +152 -0
  98. package/tui/src/util/quote-utils.ts +19 -0
  99. package/tui/src/util/room-editor.tsx +164 -0
  100. package/tui/src/util/room-logic.ts +8 -0
  101. package/tui/src/util/room-parser.ts +70 -0
  102. 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 { sendMessage, activePersonaId, stopProcessor, showNotification } = ei;
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
- {`Personas ${isFocused() ? "[*]" : ""}`}
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 { activePersonaId, personas, queueStatus, notification } = useEi();
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
- <text fg="#586e75">
58
- <Show when={getActiveDisplayName()} fallback="No persona selected">
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
- </text>
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="#586e75" marginRight={2}>
70
- [{getFocusIndicator()}]
160
+ <text fg={centerIndicator().color} marginRight={2}>
161
+ {centerIndicator().text}
71
162
  </text>
72
163
 
73
164
  <Show when={!sidebarVisible()}>