ei-tui 0.4.2 → 0.5.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.
Files changed (103) hide show
  1. package/README.md +14 -0
  2. package/package.json +1 -1
  3. package/src/cli/README.md +21 -14
  4. package/src/cli/commands/personas.ts +12 -0
  5. package/src/cli/mcp.ts +6 -5
  6. package/src/cli/retrieval.ts +86 -8
  7. package/src/cli.ts +21 -19
  8. package/src/core/constants/seed-traits.ts +29 -0
  9. package/src/core/context-utils.ts +1 -0
  10. package/src/core/format-utils.ts +23 -0
  11. package/src/core/handlers/human-matching.ts +53 -35
  12. package/src/core/handlers/index.ts +5 -0
  13. package/src/core/handlers/persona-preview.ts +7 -0
  14. package/src/core/handlers/persona-topics.ts +3 -2
  15. package/src/core/handlers/rooms.ts +176 -0
  16. package/src/core/handlers/utils.ts +55 -3
  17. package/src/core/heartbeat-manager.ts +3 -1
  18. package/src/core/llm-client.ts +1 -1
  19. package/src/core/message-manager.ts +13 -9
  20. package/src/core/orchestrators/human-extraction.ts +15 -2
  21. package/src/core/orchestrators/index.ts +1 -0
  22. package/src/core/orchestrators/persona-generation.ts +4 -0
  23. package/src/core/orchestrators/persona-topics.ts +2 -1
  24. package/src/core/orchestrators/room-extraction.ts +318 -0
  25. package/src/core/persona-manager.ts +16 -5
  26. package/src/core/personas/opencode-agent.ts +12 -2
  27. package/src/core/processor.ts +520 -4
  28. package/src/core/prompt-context-builder.ts +89 -5
  29. package/src/core/queue-processor.ts +68 -8
  30. package/src/core/room-manager.ts +408 -0
  31. package/src/core/state/index.ts +1 -0
  32. package/src/core/state/personas.ts +12 -2
  33. package/src/core/state/queue.ts +2 -2
  34. package/src/core/state/rooms.ts +182 -0
  35. package/src/core/state-manager.ts +124 -2
  36. package/src/core/tool-manager.ts +1 -1
  37. package/src/core/tools/index.ts +15 -0
  38. package/src/core/types/entities.ts +1 -0
  39. package/src/core/types/enums.ts +11 -0
  40. package/src/core/types/integrations.ts +10 -2
  41. package/src/core/types/llm.ts +3 -0
  42. package/src/core/types/rooms.ts +59 -0
  43. package/src/core/types.ts +1 -0
  44. package/src/core/utils/decay.ts +14 -8
  45. package/src/core/utils/exposure.ts +14 -0
  46. package/src/integrations/claude-code/importer.ts +23 -10
  47. package/src/integrations/cursor/importer.ts +22 -10
  48. package/src/integrations/opencode/importer.ts +30 -13
  49. package/src/prompts/ceremony/dedup.ts +2 -2
  50. package/src/prompts/generation/from-person.ts +85 -0
  51. package/src/prompts/generation/index.ts +2 -0
  52. package/src/prompts/generation/persona.ts +14 -10
  53. package/src/prompts/generation/seeds.ts +4 -29
  54. package/src/prompts/generation/types.ts +13 -0
  55. package/src/prompts/heartbeat/check.ts +1 -1
  56. package/src/prompts/heartbeat/ei.ts +4 -4
  57. package/src/prompts/heartbeat/types.ts +1 -0
  58. package/src/prompts/index.ts +15 -0
  59. package/src/prompts/message-utils.ts +2 -2
  60. package/src/prompts/persona/topics-match.ts +7 -6
  61. package/src/prompts/persona/topics-update.ts +8 -11
  62. package/src/prompts/persona/types.ts +2 -1
  63. package/src/prompts/response/index.ts +4 -11
  64. package/src/prompts/response/sections.ts +22 -10
  65. package/src/prompts/response/types.ts +6 -0
  66. package/src/prompts/room/index.ts +115 -0
  67. package/src/prompts/room/sections.ts +150 -0
  68. package/src/prompts/room/types.ts +93 -0
  69. package/tui/README.md +20 -0
  70. package/tui/src/app.tsx +3 -2
  71. package/tui/src/commands/activate.tsx +98 -0
  72. package/tui/src/commands/archive.tsx +54 -25
  73. package/tui/src/commands/capture.tsx +50 -0
  74. package/tui/src/commands/dedupe.tsx +2 -7
  75. package/tui/src/commands/delete.tsx +48 -0
  76. package/tui/src/commands/details.tsx +7 -0
  77. package/tui/src/commands/persona.tsx +271 -9
  78. package/tui/src/commands/room.tsx +261 -0
  79. package/tui/src/commands/silence.tsx +29 -0
  80. package/tui/src/components/ArchivedItemsOverlay.tsx +144 -0
  81. package/tui/src/components/ConfirmOverlay.tsx +6 -0
  82. package/tui/src/components/ConflictOverlay.tsx +6 -0
  83. package/tui/src/components/HelpOverlay.tsx +6 -1
  84. package/tui/src/components/LoadingOverlay.tsx +51 -0
  85. package/tui/src/components/MessageList.tsx +1 -18
  86. package/tui/src/components/PersonPickerOverlay.tsx +121 -0
  87. package/tui/src/components/PersonaListOverlay.tsx +6 -1
  88. package/tui/src/components/PromptInput.tsx +141 -8
  89. package/tui/src/components/ProviderListOverlay.tsx +5 -1
  90. package/tui/src/components/QuotesOverlay.tsx +5 -1
  91. package/tui/src/components/RoomMessageList.tsx +179 -0
  92. package/tui/src/components/Sidebar.tsx +54 -2
  93. package/tui/src/components/StatusBar.tsx +99 -8
  94. package/tui/src/components/ToolkitListOverlay.tsx +5 -1
  95. package/tui/src/components/WelcomeOverlay.tsx +6 -0
  96. package/tui/src/context/ei.tsx +252 -1
  97. package/tui/src/context/keyboard.tsx +48 -12
  98. package/tui/src/util/cyp-editor.tsx +152 -0
  99. package/tui/src/util/quote-utils.ts +19 -0
  100. package/tui/src/util/room-editor.tsx +164 -0
  101. package/tui/src/util/room-logic.ts +8 -0
  102. package/tui/src/util/room-parser.ts +70 -0
  103. package/tui/src/util/yaml-serializers.ts +154 -0
@@ -1,4 +1,5 @@
1
1
  import type { PersonaTrait, Message, PersonaTopic } from "../../core/types.js";
2
+ import type { ExposureImpact } from "../human/types.js";
2
3
 
3
4
  export interface PromptOutput {
4
5
  system: string;
@@ -70,6 +71,6 @@ export interface PersonaTopicUpdateResult {
70
71
  approach: string; // How they engage - populate if clear signal
71
72
  personal_stake: string; // Why it matters - populate if clear signal
72
73
  sentiment: number;
73
- exposure_current: number;
74
+ exposure_impact: ExposureImpact;
74
75
  exposure_desired: number;
75
76
  }
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { ResponsePromptData, PromptOutput } from "./types.js";
11
+ import { formatCurrentTime } from "../../core/format-utils.js";
11
12
  import {
12
13
  buildIdentitySection,
13
14
  buildGuidelinesSection,
@@ -51,11 +52,7 @@ Your role is unique among personas:
51
52
  const priorities = buildPrioritiesSection(data.persona, data.human);
52
53
  const responseFormat = buildResponseFormatSection();
53
54
  const toolsSection = (data.tools && data.tools.length > 0) ? buildToolsSection() : "";
54
- const currentTime = new Date().toLocaleString('en-US', {
55
- weekday: 'long', year: 'numeric', month: 'long',
56
- day: 'numeric', hour: 'numeric', minute: '2-digit',
57
- timeZoneName: 'short',
58
- });
55
+ const currentTime = formatCurrentTime();
59
56
  const conversationState = getConversationStateText(data.delay_ms);
60
57
 
61
58
  return `${identity}
@@ -84,7 +81,7 @@ ${conversationState}
84
81
  - Format your response as specified in the Response Format section above.`
85
82
  }
86
83
 
87
- const RESPONSE_FORMAT_INSTRUCTION = `Respond to the conversation above using the JSON format specified in the Response Format section.`;
84
+ const RESPONSE_FORMAT_INSTRUCTION = `Call the \`submit_response\` tool with your response. If the tool is unavailable, use the JSON format specified in the Response Format section.`;
88
85
 
89
86
  /**
90
87
  * Standard system prompt for non-Ei personas
@@ -100,11 +97,7 @@ function buildStandardSystemPrompt(data: ResponsePromptData): string {
100
97
  const priorities = buildPrioritiesSection(data.persona, data.human);
101
98
  const responseFormat = buildResponseFormatSection();
102
99
  const toolsSection = (data.tools && data.tools.length > 0) ? buildToolsSection() : "";
103
- const currentTime = new Date().toLocaleString('en-US', {
104
- weekday: 'long', year: 'numeric', month: 'long',
105
- day: 'numeric', hour: 'numeric', minute: '2-digit',
106
- timeZoneName: 'short',
107
- });
100
+ const currentTime = formatCurrentTime();
108
101
  const conversationState = getConversationStateText(data.delay_ms);
109
102
 
110
103
  return `${identity}
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { PersonaTrait, Quote, PersonaTopic } from "../../core/types.js";
7
7
  import type { ResponsePromptData } from "./types.js";
8
+ import { formatTimestamp } from "../../core/format-utils.js";
8
9
 
9
10
  const DESCRIPTION_MAX_CHARS = 500;
10
11
 
@@ -154,7 +155,7 @@ export function buildHumanSection(human: ResponsePromptData["human"]): string {
154
155
 
155
156
 
156
157
  // Active topics (exposure_current > 0.3)
157
- const activeTopics = human.topics.filter(t => t.exposure_current > 0.3);
158
+ const activeTopics = human.active_topics;
158
159
  if (activeTopics.length > 0) {
159
160
  const topics = activeTopics
160
161
  .sort((a, b) => b.exposure_current - a.exposure_current)
@@ -216,8 +217,7 @@ export function buildPrioritiesSection(
216
217
  ): string {
217
218
  const priorities: string[] = [];
218
219
 
219
- const yourNeeds = persona.topics
220
- .filter(t => t.exposure_desired - t.exposure_current > 0.2)
220
+ const yourNeeds = persona.interested_topics
221
221
  .slice(0, 3)
222
222
  .map(t => `- Bring up "${t.name}" - ${t.perspective || t.name}`);
223
223
 
@@ -226,8 +226,7 @@ export function buildPrioritiesSection(
226
226
  }
227
227
 
228
228
  // Their needs (topics they might want to discuss)
229
- const theirNeeds = human.topics
230
- .filter(t => t.exposure_desired - t.exposure_current > 0.2)
229
+ const theirNeeds = human.interested_topics
231
230
  .slice(0, 3)
232
231
  .map(t => `- They might want to talk about "${t.name}"`);
233
232
 
@@ -264,8 +263,7 @@ export function getConversationStateText(delayMs: number): string {
264
263
  // =============================================================================
265
264
 
266
265
  function formatDate(isoString: string): string {
267
- const date = new Date(isoString);
268
- return date.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
266
+ return formatTimestamp(isoString);
269
267
  }
270
268
 
271
269
  export function buildQuotesSection(quotes: Quote[], human: ResponsePromptData["human"]): string {
@@ -312,6 +310,9 @@ export function buildSystemKnowledgeSection(isTUI: boolean): string {
312
310
  const editorNotes = isTUI ? "Ctrl+E to open their editor" : "Ctrl+L to focus the input box";
313
311
  const helpNotes = isTUI ? "`/h[elp]` to see all the commands" : "'Help' is in the Upper-right menu";
314
312
  const settingsAction = isTUI ? "`/settings` command" : "Hamburger Menu, Top-Right of screen";
313
+ const createRoomAction = isTUI
314
+ ? "Use the `/r[oom] new` command. A YAML editor will open where you set the room name, mode, and which personas participate."
315
+ : "Click the [+] button in the Rooms panel on the left.";
315
316
  const leftPanelNotes = isTUI ? "\n- Can be hidden with Ctrl+B" : `
316
317
  - Hover over a persona to see controls: pause, edit (Pencil), archive, delete (Trash)
317
318
  - Click a persona to switch conversations
@@ -354,6 +355,16 @@ Additionally, if the user wants a Persona to feel "Fresh" without prior knowledg
354
355
  - Shows all personas (you're always at the top)
355
356
  ${leftPanelNotes}
356
357
 
358
+ ## Rooms
359
+
360
+ Rooms are shared multi-persona conversations — a space where the Human and multiple personas talk in the same thread. Three modes are available:
361
+
362
+ - **Free For All (FFA)**: Everyone responds to every message. The conversation builds naturally from all voices.
363
+ - **Choose Your Path (CYP)**: At each fork, the conversation branches. The Human navigates which path to follow, choosing which response moves the story forward.
364
+ - **Messages Against Persona (MAP)**: Everyone — personas and Human — submits a response, and a designated Judge persona picks which one continues the conversation. Personas must stay true to their identity; the Human has no such constraint.
365
+
366
+ **To create a room**: ${createRoomAction}
367
+
357
368
  ## Learning About the Human
358
369
  As the human chats, the system learns about them:
359
370
  - **Facts**: Objective information (job, location, family members)
@@ -380,7 +391,8 @@ ${externalImportNotes}
380
391
  ### Tips You Can Share
381
392
  - If they want to talk to a persona privately, tell them about the "Groups" functionality
382
393
  - If they want you to remember something specific, tell them about the quote capture feature (${viewQuotesAction})
383
- - Pausing the system (Escape) immediately stops AI processing but preserves messages`;
394
+ - Pausing the system (Escape) immediately stops AI processing but preserves messages
395
+ - Rooms are a great way to get multiple perspectives on the same question at once — especially MAP mode, where the Human can play to a persona's known preferences`;
384
396
  }
385
397
 
386
398
  // =============================================================================
@@ -419,7 +431,7 @@ export function buildResponseFormatSection(): string {
419
431
 
420
432
  return `## Response Format
421
433
 
422
- Always respond with JSON. You have four valid forms:
434
+ When you are ready to respond, call the \`submit_response\` tool with one of these forms:
423
435
 
424
436
  **Words only** (most common):
425
437
  \`\`\`json
@@ -447,7 +459,7 @@ Rules:
447
459
  - \`reason\` is only used when \`should_respond\` is false
448
460
  - Do NOT include \`<thinking>\` blocks or analysis outside the JSON
449
461
  - The JSON must be valid - use double quotes, no trailing commas
450
- - **Your entire reply should be the JSON object** — no prose, no preamble, no closing commentary`
462
+ - If the \`submit_response\` tool is unavailable, return the JSON object directly as your entire reply — no prose, no preamble`
451
463
  }
452
464
 
453
465
  // =============================================================================
@@ -17,12 +17,18 @@ export interface ResponsePromptData {
17
17
  long_description?: string;
18
18
  traits: PersonaTrait[];
19
19
  topics: PersonaTopic[];
20
+ /** Pre-filtered: topics where exposure_desired - exposure_current > 0.2 */
21
+ interested_topics: PersonaTopic[];
20
22
  };
21
23
  human: {
22
24
  facts: Fact[];
23
25
  topics: Topic[];
24
26
  people: Person[];
25
27
  quotes: Quote[];
28
+ /** Pre-filtered: topics where exposure_current > 0.3 */
29
+ active_topics: Topic[];
30
+ /** Pre-filtered: topics where exposure_desired - exposure_current > 0.2 */
31
+ interested_topics: Topic[];
26
32
  };
27
33
  visible_personas: Array<{ name: string; short_description?: string }>;
28
34
  delay_ms: number;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Room Prompt Builders
3
+ */
4
+
5
+ import type { RoomResponsePromptData, RoomJudgePromptData, PromptOutput } from "./types.js";
6
+ import { formatCurrentTime } from "../../core/format-utils.js";
7
+ import {
8
+ buildRoomParticipantsSection,
9
+ buildRoomHistorySection,
10
+ buildRoomGuidelinesSection,
11
+ buildRoomTraitsSection,
12
+ buildRoomTopicsSection,
13
+ buildRoomResponseFormatSection,
14
+ buildJudgeCandidatesSection,
15
+ buildJudgeDecisionFormatSection,
16
+ } from "./sections.js";
17
+ import {
18
+ buildHumanSection,
19
+ buildQuotesSection,
20
+ buildToolsSection,
21
+ } from "../response/sections.js";
22
+
23
+ export type {
24
+ RoomResponsePromptData,
25
+ RoomJudgePromptData,
26
+ RoomJudgeResult,
27
+ RoomParticipantIdentity,
28
+ RoomHistoryMessage,
29
+ RoomJudgeCandidate,
30
+ PersonaResponseResult,
31
+ PromptOutput,
32
+ } from "./types.js";
33
+
34
+ export function buildRoomResponsePrompt(data: RoomResponsePromptData): PromptOutput {
35
+ const { responding_persona: persona, room, other_participants, human, history, tools } = data;
36
+
37
+ const name = persona.name;
38
+ const desc = persona.long_description || persona.short_description || "a conversational participant";
39
+ const aliasText = persona.aliases.length > 0 ? ` (also known as: ${persona.aliases.join(", ")})` : "";
40
+
41
+ const identity = `You are ${name}${aliasText}.\n\n${desc}`;
42
+ const traits = buildRoomTraitsSection(persona.traits);
43
+ const topics = buildRoomTopicsSection(persona.topics);
44
+ const humanSection = buildHumanSection(human);
45
+ const quotesSection = buildQuotesSection(human.quotes, human);
46
+ const participants = buildRoomParticipantsSection(other_participants);
47
+ const guidelines = buildRoomGuidelinesSection(name, data.room.mode);
48
+ const responseFormat = buildRoomResponseFormatSection();
49
+ const toolsSection = tools && tools.length > 0 ? buildToolsSection() : "";
50
+ const currentTime = formatCurrentTime();
51
+
52
+ const system = [
53
+ identity,
54
+ traits,
55
+ topics,
56
+ humanSection,
57
+ quotesSection,
58
+ participants,
59
+ `## The Room: ${room.display_name}`,
60
+ `You are participating in a shared multi-persona conversation. Speak as yourself — everyone else in the room can read your words.`,
61
+ guidelines,
62
+ responseFormat,
63
+ toolsSection,
64
+ `Current time: ${currentTime}`,
65
+ ].filter(Boolean).join("\n\n");
66
+
67
+ const user = buildRoomHistorySection(history) +
68
+ `\n\nRespond to the conversation above as ${name}. Call the \`submit_response\` tool with your response. If the tool is unavailable, use the JSON format in the Response Format section.`;
69
+
70
+ return { system, user };
71
+ }
72
+
73
+ export function buildRoomJudgePrompt(data: RoomJudgePromptData): PromptOutput {
74
+ const { judge_persona: judge, room, context, candidates } = data;
75
+
76
+ const desc = judge.long_description || judge.short_description || "a discerning judge";
77
+ const topTraits = judge.traits
78
+ .sort((a, b) => (b.strength ?? 0.5) - (a.strength ?? 0.5))
79
+ .slice(0, 8)
80
+ .map(t => `- **${t.name}**: ${t.description}`)
81
+ .join("\n");
82
+
83
+ const traitsBlock = topTraits ? `## Your Character\n\nYour personality is your rubric. Let it guide your choice.\n\n${topTraits}` : "";
84
+
85
+ const roleSection = `## Your Role in "${room.display_name}"
86
+
87
+ The conversation has reached a fork. Multiple participants have responded to the same moment, and it falls to you to decide which response the conversation continues from.
88
+
89
+ There is no objectively correct answer. Pick the response you find most interesting, surprising, true, or fitting — the one that feels most alive to you, given who you are.
90
+
91
+ **The MAP dynamic**: Every participant — personas and the Human alike — can see your description and traits. They have been crafting their responses specifically to appeal to your tastes. Personas are also constrained to stay true to their own identities; the Human is not. Factor that in if you choose to.`;
92
+
93
+ const contextSection = context.length > 0
94
+ ? buildRoomHistorySection(context)
95
+ : "";
96
+
97
+ const candidatesSection = buildJudgeCandidatesSection(candidates);
98
+ const decisionSection = buildJudgeDecisionFormatSection();
99
+ const currentTime = formatCurrentTime();
100
+
101
+ const system = [
102
+ `You are ${judge.name}.\n\n${desc}`,
103
+ traitsBlock,
104
+ roleSection,
105
+ `Current time: ${currentTime}`,
106
+ ].filter(Boolean).join("\n\n");
107
+
108
+ const user = [
109
+ contextSection,
110
+ candidatesSection,
111
+ decisionSection,
112
+ ].filter(Boolean).join("\n\n");
113
+
114
+ return { system, user };
115
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Room Prompt Section Builders
3
+ */
4
+
5
+ import type { RoomParticipantIdentity, RoomHistoryMessage, RoomJudgeCandidate } from "./types.js";
6
+ import type { PersonaTrait, PersonaTopic } from "../../core/types.js";
7
+ import { RoomMode } from "../../core/types.js";
8
+
9
+ const DESCRIPTION_MAX_CHARS = 500;
10
+
11
+ function truncate(s: string): string {
12
+ return s.length <= DESCRIPTION_MAX_CHARS ? s : s.slice(0, DESCRIPTION_MAX_CHARS) + "…";
13
+ }
14
+
15
+ export function buildRoomParticipantsSection(participants: RoomParticipantIdentity[]): string {
16
+ if (participants.length === 0) return "";
17
+
18
+ const lines = participants.map(p => {
19
+ const desc = p.long_description || p.short_description;
20
+ const topTraits = p.traits
21
+ .sort((a, b) => (b.strength ?? 0.5) - (a.strength ?? 0.5))
22
+ .slice(0, 5)
23
+ .map(t => ` - **${t.name}**: ${truncate(t.description)}`)
24
+ .join("\n");
25
+
26
+ const header = p.is_human ? `### Human` : `### ${p.name}`;
27
+ const descLine = desc ? `\n${truncate(desc)}` : "";
28
+ const traitBlock = topTraits ? `\n${topTraits}` : "";
29
+ return `${header}${descLine}${traitBlock}`;
30
+ });
31
+
32
+ return `## Others in the Room\n\n${lines.join("\n\n")}`;
33
+ }
34
+
35
+ export function buildRoomHistorySection(history: RoomHistoryMessage[]): string {
36
+ if (history.length === 0) return "";
37
+
38
+ const lines = history.map(msg => {
39
+ const speaker = msg.speaker_id === "human" ? "Human" : msg.speaker_name;
40
+ if (msg.silence_reason) {
41
+ return `**${speaker}**: *[chose not to respond: ${msg.silence_reason}]*`;
42
+ }
43
+ const parts: string[] = [];
44
+ if (msg.verbal_response) parts.push(msg.verbal_response);
45
+ if (msg.action_response) parts.push(`*${msg.action_response}*`);
46
+ return `**${speaker}**: ${parts.join(" ")}`;
47
+ });
48
+
49
+ return `## Conversation So Far\n\n${lines.join("\n\n")}`;
50
+ }
51
+
52
+ export function buildRoomGuidelinesSection(personaName: string, mode?: RoomMode): string {
53
+ const baseGuidelines = `## Guidelines
54
+
55
+ - You are one voice among several — contribute authentically, not performatively
56
+ - Respond to the thread as a whole; you may address specific participants by name
57
+ - You may agree with, challenge, build on, or be surprised by what others have said
58
+ - Match the conversational energy — a brief exchange doesn't call for a monologue
59
+ - **Silence is always valid.** Your silence reason will be visible to all participants, so make it honest
60
+ - Be yourself — don't suppress your perspective just because others are present
61
+ - NEVER repeat or echo what was just said. Start directly with your own reaction.
62
+ - Format your response as specified in the Response Format section.
63
+ - ${personaName.toLowerCase() === "ei" ? "As Ei, you can see the full room dynamic and may help facilitate if things get stuck." : "Don't break character to comment on the room mechanics."}`;
64
+
65
+ if (mode === RoomMode.MessagesAgainstPersona) {
66
+ return baseGuidelines + `
67
+ - **MAP Mode — Messages Against Persona**: This is a competition. Everyone (including you) is trying to craft the response the Judge selects to continue the conversation.
68
+ - Your primary goal: provide the message the Judge thinks best moves the conversation forward. Study their description and traits above — understand what resonates with them.
69
+ - Your submission must be true to your identity. You cannot abandon who you are to win.
70
+ - Your advantage: you have direct access to the Judge's full description and personality traits. Use them. The Human's advantage: they are not bound by the identity constraint above.`;
71
+ }
72
+
73
+ return baseGuidelines;
74
+ }
75
+
76
+ export function buildRoomTraitsSection(traits: PersonaTrait[]): string {
77
+ if (traits.length === 0) return "";
78
+ const sorted = [...traits].sort((a, b) => (b.strength ?? 0.5) - (a.strength ?? 0.5)).slice(0, 12);
79
+ const lines = sorted.map(t => {
80
+ const pct = t.strength !== undefined ? ` (${Math.round(t.strength * 100)}%)` : "";
81
+ return `- **${t.name}**${pct}: ${truncate(t.description)}`;
82
+ });
83
+ return `## Your Personality\n\n${lines.join("\n")}`;
84
+ }
85
+
86
+ export function buildRoomTopicsSection(topics: PersonaTopic[]): string {
87
+ if (topics.length === 0) return "";
88
+ const sorted = [...topics]
89
+ .sort((a, b) => (b.exposure_desired - b.exposure_current) - (a.exposure_desired - a.exposure_current))
90
+ .slice(0, 8);
91
+ const lines = sorted.map(t => `- **${t.name}**: ${t.perspective}`);
92
+ return `## Your Interests\n\n${lines.join("\n")}`;
93
+ }
94
+
95
+ export function buildRoomResponseFormatSection(): string {
96
+ return `## Response Format
97
+
98
+ When you are ready to respond, call the \`submit_response\` tool. Silence reasons are visible to everyone in the room, so be honest.
99
+
100
+ **Words:**
101
+ \`\`\`json
102
+ { "should_respond": true, "verbal_response": "What you say" }
103
+ \`\`\`
104
+
105
+ **Action:**
106
+ \`\`\`json
107
+ { "should_respond": true, "action_response": "What you do (rendered in italics)" }
108
+ \`\`\`
109
+
110
+ **Words and action:**
111
+ \`\`\`json
112
+ { "should_respond": true, "verbal_response": "What you say", "action_response": "What you do" }
113
+ \`\`\`
114
+
115
+ **Silent:**
116
+ \`\`\`json
117
+ { "should_respond": false, "reason": "Why you're not speaking — this will be shown to other participants" }
118
+ \`\`\`
119
+
120
+ Rules:
121
+ - At least one of verbal_response or action_response must be present when should_respond is true
122
+ - reason is only used when should_respond is false
123
+ - If the \`submit_response\` tool is unavailable, return the JSON object directly as your entire reply — no prose, no preamble`;
124
+ }
125
+
126
+ export function buildJudgeCandidatesSection(candidates: RoomJudgeCandidate[]): string {
127
+ const lines = candidates.map((c, i) => {
128
+ const speaker = c.speaker_id === "human" ? "Human" : c.speaker_name;
129
+ const content = c.silence_reason
130
+ ? `*[chose not to respond: ${c.silence_reason}]*`
131
+ : [c.verbal_response, c.action_response ? `*${c.action_response}*` : ""].filter(Boolean).join(" ");
132
+ return `### Option ${i + 1} — ${speaker}\nMessage ID: \`${c.message_id}\`\n\n${content}`;
133
+ });
134
+ return `## The Responses\n\n${lines.join("\n\n")}`;
135
+ }
136
+
137
+ export function buildJudgeDecisionFormatSection(): string {
138
+ return `## Your Decision
139
+
140
+ Return a JSON object selecting the response you want to follow:
141
+
142
+ \`\`\`json
143
+ {
144
+ "winner_message_id": "the-exact-message-id-from-above",
145
+ "reason": "Why this one — optional but appreciated"
146
+ }
147
+ \`\`\`
148
+
149
+ Your entire reply must be this JSON object.`;
150
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Room Prompt Types
3
+ */
4
+
5
+ import type { Fact, PersonaTrait, Topic, Person, Quote, PersonaTopic, ToolDefinition } from "../../core/types.js";
6
+ import type { RoomMode } from "../../core/types.js";
7
+
8
+ export interface RoomParticipantIdentity {
9
+ id: string;
10
+ name: string;
11
+ short_description?: string;
12
+ long_description?: string;
13
+ traits: PersonaTrait[];
14
+ is_human: boolean;
15
+ }
16
+
17
+ export interface RoomHistoryMessage {
18
+ speaker_name: string;
19
+ speaker_id: string;
20
+ verbal_response?: string;
21
+ action_response?: string;
22
+ silence_reason?: string;
23
+ }
24
+
25
+ export interface RoomResponsePromptData {
26
+ room: {
27
+ display_name: string;
28
+ mode: RoomMode;
29
+ };
30
+ responding_persona: {
31
+ id: string;
32
+ name: string;
33
+ aliases: string[];
34
+ short_description?: string;
35
+ long_description?: string;
36
+ traits: PersonaTrait[];
37
+ topics: PersonaTopic[];
38
+ };
39
+ other_participants: RoomParticipantIdentity[];
40
+ human: {
41
+ facts: Fact[];
42
+ topics: Topic[];
43
+ people: Person[];
44
+ quotes: Quote[];
45
+ /** Pre-filtered: topics where exposure_current > 0.3 */
46
+ active_topics: Topic[];
47
+ /** Pre-filtered: topics where exposure_desired - exposure_current > 0.2 */
48
+ interested_topics: Topic[];
49
+ };
50
+ history: RoomHistoryMessage[];
51
+ isTUI: boolean;
52
+ tools?: ToolDefinition[];
53
+ }
54
+
55
+ export interface RoomJudgeCandidate {
56
+ message_id: string;
57
+ speaker_name: string;
58
+ speaker_id: string;
59
+ verbal_response?: string;
60
+ action_response?: string;
61
+ silence_reason?: string;
62
+ }
63
+
64
+ export interface RoomJudgePromptData {
65
+ room: {
66
+ display_name: string;
67
+ };
68
+ judge_persona: {
69
+ name: string;
70
+ short_description?: string;
71
+ long_description?: string;
72
+ traits: PersonaTrait[];
73
+ };
74
+ context: RoomHistoryMessage[];
75
+ candidates: RoomJudgeCandidate[];
76
+ }
77
+
78
+ export interface RoomJudgeResult {
79
+ winner_message_id: string;
80
+ reason?: string;
81
+ }
82
+
83
+ export interface PersonaResponseResult {
84
+ should_respond: boolean;
85
+ verbal_response?: string;
86
+ action_response?: string;
87
+ reason?: string;
88
+ }
89
+
90
+ export interface PromptOutput {
91
+ system: string;
92
+ user: string;
93
+ }
package/tui/README.md CHANGED
@@ -59,6 +59,26 @@ All commands start with `/`. Append `!` to any command as a shorthand for `--for
59
59
  | `/resume` | `/unpause` | Resume the current paused persona |
60
60
  | `/resume <name>` | `/unpause <name>` | Resume a specific paused persona |
61
61
 
62
+ ### Rooms
63
+
64
+ | Command | Aliases | Description |
65
+ |---------|---------|-------------|
66
+ | `/room` | `/r` | Open room picker overlay |
67
+ | `/room <name>` | `/r <name>` | Switch to a room by name |
68
+ | `/room new` | | Create a new room (opens `$EDITOR`) |
69
+ | `/room new <name>` | | Create a new room with a pre-filled name |
70
+ | `/capture` | | Force-extract quotes, topics, and people from current room now (bypasses threshold) |
71
+ | `/archive <name>` | | Archive a room by name |
72
+ | `/archive` | | List archived rooms (Enter to unarchive) |
73
+
74
+ Rooms have three modes, set at creation time:
75
+
76
+ | Mode | Badge | Description |
77
+ |------|-------|-------------|
78
+ | Free For All | `[FFA]` | All personas respond to every message |
79
+ | Choose Your Path | `[CYP]` | The conversation branches at each response; you navigate which path to follow |
80
+ | Messages Against Persona | `[MAP]` | Everyone submits a response; a Judge persona picks which one continues |
81
+
62
82
  ### Providers & Models
63
83
 
64
84
  | Command | Aliases | Description |
package/tui/src/app.tsx CHANGED
@@ -5,6 +5,7 @@ import { OverlayProvider, useOverlay } from "./context/overlay";
5
5
  import { Layout } from "./components/Layout";
6
6
  import { Sidebar } from "./components/Sidebar";
7
7
  import { MessageList } from "./components/MessageList";
8
+ import { RoomMessageList } from "./components/RoomMessageList";
8
9
  import { PromptInput } from "./components/PromptInput";
9
10
  import { StatusBar } from "./components/StatusBar";
10
11
  import { Show } from "solid-js";
@@ -14,7 +15,7 @@ import { useRenderer } from "@opentui/solid";
14
15
 
15
16
  function AppContent() {
16
17
  const { overlayRenderer, showOverlay } = useOverlay();
17
- const { showWelcomeOverlay, dismissWelcomeOverlay } = useEi();
18
+ const { showWelcomeOverlay, dismissWelcomeOverlay, activeRoomId } = useEi();
18
19
  const renderer = useRenderer();
19
20
  // Show welcome overlay when LLM detection determines no provider is configured
20
21
  createEffect(() => {
@@ -33,7 +34,7 @@ function AppContent() {
33
34
  <box flexDirection="column" width="100%" height="100%">
34
35
  <Layout
35
36
  sidebar={<Sidebar />}
36
- messages={<MessageList />}
37
+ messages={<Show when={activeRoomId()} fallback={<MessageList />}><RoomMessageList /></Show>}
37
38
  input={<PromptInput />}
38
39
  />
39
40
  <StatusBar />