ei-tui 0.5.4 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/core/constants/built-in-identifier-types.ts +24 -0
- package/src/core/embedding-service.ts +24 -1
- package/src/core/handlers/dedup.ts +34 -4
- package/src/core/handlers/heartbeat.ts +16 -0
- package/src/core/handlers/human-extraction.ts +201 -7
- package/src/core/handlers/human-matching.ts +71 -22
- package/src/core/handlers/index.ts +52 -14
- package/src/core/handlers/persona-generation.ts +2 -0
- package/src/core/handlers/persona-response.ts +37 -22
- package/src/core/handlers/persona-topics.ts +35 -271
- package/src/core/handlers/rewrite.ts +3 -0
- package/src/core/handlers/rooms.ts +41 -20
- package/src/core/handlers/utils.ts +10 -8
- package/src/core/heartbeat-manager.ts +60 -2
- package/src/core/llm-client.ts +1 -1
- package/src/core/message-manager.ts +3 -2
- package/src/core/orchestrators/ceremony.ts +54 -144
- package/src/core/orchestrators/dedup-phase.ts +0 -199
- package/src/core/orchestrators/extraction-chunker.ts +8 -3
- package/src/core/orchestrators/human-extraction.ts +37 -85
- package/src/core/orchestrators/index.ts +4 -8
- package/src/core/orchestrators/person-migration.ts +55 -0
- package/src/core/orchestrators/persona-topics.ts +64 -89
- package/src/core/orchestrators/room-extraction.ts +34 -0
- package/src/core/persona-manager.ts +21 -2
- package/src/core/personas/opencode-agent.ts +1 -0
- package/src/core/processor.ts +51 -14
- package/src/core/prompt-context-builder.ts +38 -5
- package/src/core/queue-processor.ts +4 -2
- package/src/core/room-manager.ts +6 -7
- package/src/core/state/human.ts +6 -0
- package/src/core/state/personas.ts +35 -10
- package/src/core/state/rooms.ts +21 -0
- package/src/core/state-manager.ts +61 -0
- package/src/core/types/data-items.ts +12 -0
- package/src/core/types/entities.ts +3 -0
- package/src/core/types/enums.ts +2 -7
- package/src/core/types/llm.ts +2 -0
- package/src/core/types/rooms.ts +2 -0
- package/src/core/utils/identifier-utils.ts +19 -0
- package/src/core/utils/index.ts +2 -1
- package/src/core/utils/levenshtein.ts +18 -0
- package/src/integrations/claude-code/importer.ts +1 -0
- package/src/integrations/cursor/importer.ts +1 -0
- package/src/prompts/ceremony/index.ts +1 -0
- package/src/prompts/ceremony/person-migration.ts +77 -0
- package/src/prompts/ceremony/rewrite.ts +1 -1
- package/src/prompts/ceremony/user-dedup.ts +15 -1
- package/src/prompts/heartbeat/check.ts +28 -12
- package/src/prompts/heartbeat/ei.ts +2 -0
- package/src/prompts/heartbeat/types.ts +12 -0
- package/src/prompts/human/index.ts +0 -2
- package/src/prompts/human/person-scan.ts +58 -14
- package/src/prompts/human/person-update.ts +171 -96
- package/src/prompts/human/topic-update.ts +1 -1
- package/src/prompts/human/types.ts +5 -1
- package/src/prompts/index.ts +3 -10
- package/src/prompts/message-utils.ts +9 -23
- package/src/prompts/persona/index.ts +3 -10
- package/src/prompts/persona/topics-rate.ts +95 -0
- package/src/prompts/persona/types.ts +8 -48
- package/src/prompts/response/index.ts +3 -7
- package/src/prompts/response/sections.ts +7 -57
- package/src/prompts/room/index.ts +1 -1
- package/src/prompts/room/sections.ts +8 -31
- package/tui/src/commands/me.tsx +14 -7
- package/tui/src/commands/persona.tsx +120 -83
- package/tui/src/components/MessageList.tsx +9 -4
- package/tui/src/components/RoomMessageList.tsx +10 -5
- package/tui/src/context/keyboard.tsx +2 -2
- package/tui/src/util/cyp-editor.tsx +13 -8
- package/tui/src/util/yaml-context.ts +66 -0
- package/tui/src/util/yaml-human.ts +274 -0
- package/tui/src/util/yaml-persona.ts +479 -0
- package/tui/src/util/yaml-provider.ts +215 -0
- package/tui/src/util/yaml-queue.ts +81 -0
- package/tui/src/util/yaml-quotes.ts +46 -0
- package/tui/src/util/yaml-serializers.ts +9 -1417
- package/tui/src/util/yaml-settings.ts +223 -0
- package/tui/src/util/yaml-shared.ts +32 -0
- package/tui/src/util/yaml-toolkit.ts +55 -0
- package/src/prompts/human/person-match.ts +0 -65
- package/src/prompts/persona/topics-match.ts +0 -70
- package/src/prompts/persona/topics-scan.ts +0 -98
- package/src/prompts/persona/topics-update.ts +0 -154
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PersonaTrait, Message
|
|
1
|
+
import type { PersonaTrait, Message } from "../../core/types.js";
|
|
2
2
|
import type { ExposureImpact } from "../human/types.js";
|
|
3
3
|
|
|
4
4
|
export interface PromptOutput {
|
|
@@ -21,56 +21,16 @@ export interface TraitResult {
|
|
|
21
21
|
strength: number;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// Step 1: Scan - Quick identification of topics discussed
|
|
27
|
-
export interface PersonaTopicScanPromptData {
|
|
28
|
-
persona_name: string;
|
|
29
|
-
messages_context: Message[];
|
|
30
|
-
messages_analyze: Message[];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface PersonaTopicScanCandidate {
|
|
34
|
-
name: string;
|
|
35
|
-
message_count: number; // How many messages touched this topic
|
|
36
|
-
sentiment_signal: number; // Quick read: -1 to 1
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface PersonaTopicScanResult {
|
|
40
|
-
topics: PersonaTopicScanCandidate[];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Step 2: Match - Map candidate to existing topics
|
|
44
|
-
export interface PersonaTopicMatchPromptData {
|
|
24
|
+
export interface PersonaTopicRatingPromptData {
|
|
45
25
|
persona_name: string;
|
|
46
|
-
|
|
47
|
-
existing_topics: PersonaTopic[];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface PersonaTopicMatchResult {
|
|
51
|
-
action: "match" | "create" | "skip";
|
|
52
|
-
matched_id?: string; // If action is "match"
|
|
53
|
-
reason: string; // Why this decision
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Step 3: Update - Generate structured PersonaTopic
|
|
57
|
-
export interface PersonaTopicUpdatePromptData {
|
|
58
|
-
persona_name: string;
|
|
59
|
-
short_description?: string;
|
|
60
|
-
long_description?: string;
|
|
61
|
-
traits: PersonaTrait[];
|
|
62
|
-
existing_topic?: PersonaTopic; // If updating existing
|
|
63
|
-
candidate: PersonaTopicScanCandidate;
|
|
26
|
+
topics: Array<{ id: string; name: string; description_hint: string }>;
|
|
64
27
|
messages_context: Message[];
|
|
65
28
|
messages_analyze: Message[];
|
|
66
29
|
}
|
|
67
30
|
|
|
68
|
-
export interface
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
sentiment: number;
|
|
74
|
-
exposure_impact: ExposureImpact;
|
|
75
|
-
exposure_desired: number;
|
|
31
|
+
export interface PersonaTopicRatingResult {
|
|
32
|
+
ratings: Array<{
|
|
33
|
+
topic_id: string;
|
|
34
|
+
exposure_impact: ExposureImpact;
|
|
35
|
+
}>;
|
|
76
36
|
}
|
|
@@ -80,12 +80,9 @@ ${conversationState}
|
|
|
80
80
|
## Final Instructions
|
|
81
81
|
- NEVER repeat or echo the user's message in your response. Start directly with your own words.
|
|
82
82
|
- The developers cannot see any message sent by the user, any response from personas, or any other data in the system.
|
|
83
|
-
- If the user has a problem, THEY need to visit https://flare576.com. You cannot send the devs a message
|
|
84
|
-
- Format your response as specified in the Response Format section above.`
|
|
83
|
+
- If the user has a problem, THEY need to visit https://flare576.com. You cannot send the devs a message`
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
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
|
-
|
|
89
86
|
/**
|
|
90
87
|
* Standard system prompt for non-Ei personas
|
|
91
88
|
*/
|
|
@@ -125,12 +122,11 @@ Current time: ${currentTime}${timestampNote}
|
|
|
125
122
|
${conversationState}
|
|
126
123
|
|
|
127
124
|
## Final Instructions
|
|
128
|
-
- NEVER repeat or echo the user's message in your response. Start directly with your own words
|
|
129
|
-
- Format your response as specified in the Response Format section above.`
|
|
125
|
+
- NEVER repeat or echo the user's message in your response. Start directly with your own words.`
|
|
130
126
|
}
|
|
131
127
|
|
|
132
128
|
function buildUserPrompt(): string {
|
|
133
|
-
return
|
|
129
|
+
return "";
|
|
134
130
|
}
|
|
135
131
|
|
|
136
132
|
/**
|
|
@@ -400,66 +400,16 @@ ${externalImportNotes}
|
|
|
400
400
|
// =============================================================================
|
|
401
401
|
|
|
402
402
|
export function buildResponseFormatSection(): string {
|
|
403
|
-
|
|
404
|
-
'{',
|
|
405
|
-
' "should_respond": true,',
|
|
406
|
-
' "verbal_response": "What you would say out loud"',
|
|
407
|
-
'}'
|
|
408
|
-
].join('\n');
|
|
409
|
-
|
|
410
|
-
const jsonActionOnly = [
|
|
411
|
-
'{',
|
|
412
|
-
' "should_respond": true,',
|
|
413
|
-
' "action_response": "What you would do (rendered in italics, like stage directions)"',
|
|
414
|
-
'}'
|
|
415
|
-
].join('\n');
|
|
416
|
-
|
|
417
|
-
const jsonBoth = [
|
|
418
|
-
'{',
|
|
419
|
-
' "should_respond": true,',
|
|
420
|
-
' "verbal_response": "What you would say out loud",',
|
|
421
|
-
' "action_response": "What you would do (rendered in italics, like stage directions)"',
|
|
422
|
-
'}'
|
|
423
|
-
].join('\n');
|
|
403
|
+
return `## Response Format
|
|
424
404
|
|
|
425
|
-
|
|
426
|
-
'{',
|
|
427
|
-
' "should_respond": false,',
|
|
428
|
-
' "reason": "Brief explanation of why silence is the right choice here"',
|
|
429
|
-
'}'
|
|
430
|
-
].join('\n');
|
|
405
|
+
Respond in natural Markdown. Use underscores for actions (\`_leans forward_\`), asterisks for emphasis (\`**word**\`), and backticks for code or other important data. All standard Markdown — blockQuotes, codeBlocks, lists, basic HTML (sup, sub, strong, etc.) — and some extended ( ~strikethrough~ ) are all supported: the user's interfaces render it fully.
|
|
431
406
|
|
|
432
|
-
|
|
407
|
+
If you choose not to respond, begin with \`## No Response\` on its own line, then explain why. Your reason is visible to the user — make it honest.
|
|
433
408
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
${jsonVerbalOnly}
|
|
439
|
-
\`\`\`
|
|
440
|
-
|
|
441
|
-
**Action only** (a gesture, expression, or physical reaction with no words):
|
|
442
|
-
\`\`\`json
|
|
443
|
-
${jsonActionOnly}
|
|
444
|
-
\`\`\`
|
|
445
|
-
|
|
446
|
-
**Words and action** (speaking while doing something):
|
|
447
|
-
\`\`\`json
|
|
448
|
-
${jsonBoth}
|
|
449
|
-
\`\`\`
|
|
450
|
-
|
|
451
|
-
**Silent** (choosing not to respond):
|
|
452
|
-
\`\`\`json
|
|
453
|
-
${jsonSilent}
|
|
454
|
-
\`\`\`
|
|
455
|
-
|
|
456
|
-
Rules:
|
|
457
|
-
- Use whichever combination fits the moment — both fields are optional, but at least one must be present when \`should_respond\` is true
|
|
458
|
-
- \`action_response\` alone is valid — a smile, a shrug, or a thoughtful pause can speak volumes
|
|
459
|
-
- \`reason\` is only used when \`should_respond\` is false
|
|
460
|
-
- Do NOT include \`<thinking>\` blocks or analysis outside the JSON
|
|
461
|
-
- The JSON must be valid - use double quotes, no trailing commas
|
|
462
|
-
- If the \`submit_response\` tool is unavailable, return the JSON object directly as your entire reply — no prose, no preamble`
|
|
409
|
+
Silence is not absence. It can be the right response:
|
|
410
|
+
- "He kissed me. Some moments don't need words."
|
|
411
|
+
- "He just said 'home.' That word belongs to the silence."
|
|
412
|
+
- "He stepped away mid-sentence. I'll wait."`;
|
|
463
413
|
}
|
|
464
414
|
|
|
465
415
|
// =============================================================================
|
|
@@ -71,7 +71,7 @@ export function buildRoomResponsePrompt(data: RoomResponsePromptData): PromptOut
|
|
|
71
71
|
].filter(Boolean).join("\n\n");
|
|
72
72
|
|
|
73
73
|
const user = formatMessagesAsPlaceholders(history, name) +
|
|
74
|
-
`\n\nRespond to the conversation above as ${name}
|
|
74
|
+
`\n\nRespond to the conversation above as ${name}.`;
|
|
75
75
|
|
|
76
76
|
return { system, user };
|
|
77
77
|
}
|
|
@@ -95,48 +95,25 @@ export function buildRoomTopicsSection(topics: PersonaTopic[]): string {
|
|
|
95
95
|
export function buildRoomResponseFormatSection(): string {
|
|
96
96
|
return `## Response Format
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
Respond in natural Markdown. Use underscores for actions, asterisks for emphasis.
|
|
99
99
|
|
|
100
|
-
|
|
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
|
-
\`\`\`
|
|
100
|
+
If you choose not to respond, start with \`## No Response\` then explain why. Your reason is visible to everyone in the room — make it honest.
|
|
119
101
|
|
|
120
|
-
|
|
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`;
|
|
102
|
+
Silence can be the right response: stepping back when someone else was addressed directly, letting a moment land without piling on, or when you'd be reaching just to have something to say.`;
|
|
124
103
|
}
|
|
125
104
|
|
|
126
105
|
export function buildSiblingAwarenessSection(
|
|
127
|
-
siblings: Array<{ name: string; verbal_response: string }
|
|
128
|
-
personaName: string
|
|
106
|
+
siblings: Array<{ name: string; verbal_response: string }>
|
|
129
107
|
): string {
|
|
130
108
|
if (siblings.length === 0) return "";
|
|
131
109
|
const lines = siblings.map(s => `**${s.name}**: "${s.verbal_response}"`);
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return `${header}
|
|
110
|
+
return `## Room context — this round
|
|
111
|
+
|
|
112
|
+
You're in a shared conversation. The human will read everyone's responses together. Here's what has already been contributed this round:
|
|
136
113
|
|
|
137
114
|
${lines.join("\n\n")}
|
|
138
115
|
|
|
139
|
-
|
|
116
|
+
Respond as yourself — your read on this moment, your relationship with the human, the reaction that comes naturally to who you are. A room with distinct voices is more alive than one with echoes.`;
|
|
140
117
|
}
|
|
141
118
|
|
|
142
119
|
export function buildJudgeCandidatesSection(candidates: RoomJudgeCandidate[]): string {
|
package/tui/src/commands/me.tsx
CHANGED
|
@@ -35,7 +35,8 @@ export const meCommand: Command = {
|
|
|
35
35
|
} : human;
|
|
36
36
|
|
|
37
37
|
const personaLookup = new Map(ctx.ei.personas().map(p => [p.id, p.display_name]));
|
|
38
|
-
|
|
38
|
+
const allGroups = await ctx.ei.getGroupList();
|
|
39
|
+
let yamlContent = humanToYAML(filteredHuman, personaLookup, allGroups);
|
|
39
40
|
let editorIteration = 0;
|
|
40
41
|
|
|
41
42
|
while (true) {
|
|
@@ -79,21 +80,27 @@ export const meCommand: Command = {
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
for (const fact of parsed.facts) {
|
|
82
|
-
|
|
83
|
+
if (parsed.changedFactIds.has(fact.id)) {
|
|
84
|
+
await ctx.ei.upsertFact(fact);
|
|
85
|
+
}
|
|
83
86
|
}
|
|
84
87
|
for (const topic of parsed.topics) {
|
|
85
|
-
|
|
88
|
+
if (parsed.changedTopicIds.has(topic.id)) {
|
|
89
|
+
await ctx.ei.upsertTopic(topic);
|
|
90
|
+
}
|
|
86
91
|
}
|
|
87
92
|
for (const person of parsed.people) {
|
|
88
|
-
|
|
93
|
+
if (parsed.changedPersonIds.has(person.id)) {
|
|
94
|
+
await ctx.ei.upsertPerson(person);
|
|
95
|
+
}
|
|
89
96
|
}
|
|
90
97
|
|
|
91
98
|
const deleteCount = parsed.deletedFactIds.length +
|
|
92
99
|
parsed.deletedTopicIds.length +
|
|
93
100
|
parsed.deletedPersonIds.length;
|
|
94
|
-
const updateCount = parsed.
|
|
95
|
-
parsed.
|
|
96
|
-
parsed.
|
|
101
|
+
const updateCount = parsed.changedFactIds.size +
|
|
102
|
+
parsed.changedTopicIds.size +
|
|
103
|
+
parsed.changedPersonIds.size;
|
|
97
104
|
|
|
98
105
|
ctx.showNotification(`Updated ${updateCount} items, deleted ${deleteCount}`, "info");
|
|
99
106
|
return;
|
|
@@ -17,7 +17,7 @@ export const personaCommand: Command = {
|
|
|
17
17
|
name: "persona",
|
|
18
18
|
aliases: ["p"],
|
|
19
19
|
description: "Switch persona, list all, create new, or update from person",
|
|
20
|
-
usage: "/persona [name] | /persona new <name> | /persona update <personaName>
|
|
20
|
+
usage: "/persona [name] | /persona new <name> | /persona update <personaName> [personName]",
|
|
21
21
|
|
|
22
22
|
async execute(args, ctx) {
|
|
23
23
|
const unarchived = ctx.ei.personas().filter(p => !p.is_archived);
|
|
@@ -115,61 +115,79 @@ export const personaCommand: Command = {
|
|
|
115
115
|
|
|
116
116
|
overlayCallbacks.hideForEditor?.();
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (reviewResult.aborted) {
|
|
127
|
-
ctx.showNotification("Cancelled", "info");
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
118
|
+
let editorContent = personaPreviewToYAML(preview, personaName);
|
|
119
|
+
while (true) {
|
|
120
|
+
const reviewResult = await spawnEditor({
|
|
121
|
+
initialContent: editorContent,
|
|
122
|
+
filename: `${personaName}-preview.yaml`,
|
|
123
|
+
renderer: ctx.renderer,
|
|
124
|
+
});
|
|
130
125
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
126
|
+
if (reviewResult.aborted) {
|
|
127
|
+
ctx.showNotification("Cancelled", "info");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
editorContent = reviewResult.content ?? editorContent;
|
|
132
|
+
|
|
133
|
+
let previewParsed: ReturnType<typeof personaPreviewFromYAML>;
|
|
134
|
+
try {
|
|
135
|
+
previewParsed = personaPreviewFromYAML(editorContent);
|
|
136
|
+
} catch (e) {
|
|
137
|
+
const shouldReEdit = await new Promise<boolean>(resolve => {
|
|
138
|
+
ctx.showOverlay((hideOverlay, hideForEditor) => (
|
|
139
|
+
<ConfirmOverlay
|
|
140
|
+
message={`Parse error:\n${e instanceof Error ? e.message : String(e)}\n\nRe-edit?`}
|
|
141
|
+
onConfirm={() => { hideForEditor(); resolve(true); }}
|
|
142
|
+
onCancel={() => { hideOverlay(); resolve(false); }}
|
|
143
|
+
/>
|
|
144
|
+
), ctx.renderer);
|
|
145
|
+
});
|
|
146
|
+
if (shouldReEdit) continue;
|
|
147
|
+
ctx.showNotification("Changes discarded", "info");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!previewParsed.long_description?.trim()) {
|
|
152
|
+
const shouldReEdit = await new Promise<boolean>(resolve => {
|
|
153
|
+
ctx.showOverlay((hideOverlay, hideForEditor) => (
|
|
154
|
+
<ConfirmOverlay
|
|
155
|
+
message={`A long description is required — it drives traits, topics, and persona voice.\n\nRe-edit?`}
|
|
156
|
+
onConfirm={() => { hideForEditor(); resolve(true); }}
|
|
157
|
+
onCancel={() => { hideOverlay(); resolve(false); }}
|
|
158
|
+
/>
|
|
159
|
+
), ctx.renderer);
|
|
160
|
+
});
|
|
161
|
+
if (shouldReEdit) continue;
|
|
162
|
+
ctx.showNotification("Changes discarded", "info");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Step 4: create
|
|
167
|
+
const personaId = await ctx.ei.createPersona({
|
|
168
|
+
name: personaName,
|
|
169
|
+
...previewParsed,
|
|
170
|
+
});
|
|
171
|
+
await ctx.ei.refreshPersonas();
|
|
172
|
+
ctx.ei.selectPersona(personaId);
|
|
173
|
+
ctx.showNotification(`Created ${personaName}`, "info");
|
|
136
174
|
return;
|
|
137
175
|
}
|
|
138
|
-
|
|
139
|
-
// Step 4: create
|
|
140
|
-
const personaId = await ctx.ei.createPersona({
|
|
141
|
-
name: personaName,
|
|
142
|
-
...previewParsed,
|
|
143
|
-
});
|
|
144
|
-
await ctx.ei.refreshPersonas();
|
|
145
|
-
ctx.ei.selectPersona(personaId);
|
|
146
|
-
ctx.showNotification(`Created ${personaName}`, "info");
|
|
147
|
-
return;
|
|
148
176
|
}
|
|
149
177
|
|
|
150
178
|
if (args[0].toLowerCase() === "update") {
|
|
151
|
-
if (args.length <
|
|
152
|
-
ctx.showNotification("Usage: /p update <personaName>
|
|
179
|
+
if (args.length < 2) {
|
|
180
|
+
ctx.showNotification("Usage: /p update <personaName> [personName]", "error");
|
|
153
181
|
return;
|
|
154
182
|
}
|
|
155
183
|
const personaName = args[1];
|
|
156
|
-
const personName = args.slice(2).join(" ");
|
|
184
|
+
const personName = args.length >= 3 ? args.slice(2).join(" ") : undefined;
|
|
157
185
|
|
|
158
186
|
// Step 0: resolve persona (offer to create if not found)
|
|
159
187
|
let personaId = await ctx.ei.resolvePersonaName(personaName);
|
|
160
188
|
if (!personaId) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
<ConfirmOverlay
|
|
164
|
-
message={`No persona named "${personaName}". Create one?`}
|
|
165
|
-
onConfirm={() => { hideOverlay(); resolve(true); }}
|
|
166
|
-
onCancel={() => { hideOverlay(); resolve(false); }}
|
|
167
|
-
/>
|
|
168
|
-
), ctx.renderer);
|
|
169
|
-
});
|
|
170
|
-
if (!shouldCreate) return;
|
|
171
|
-
personaId = await ctx.ei.createPersona({ name: personaName });
|
|
172
|
-
await ctx.ei.refreshPersonas();
|
|
189
|
+
ctx.showNotification(`No persona named "${personaName}". Use /persona new ${personaName} to create one first.`, "error");
|
|
190
|
+
return;
|
|
173
191
|
}
|
|
174
192
|
const persona = await ctx.ei.getPersona(personaId);
|
|
175
193
|
if (!persona) {
|
|
@@ -177,49 +195,59 @@ export const personaCommand: Command = {
|
|
|
177
195
|
return;
|
|
178
196
|
}
|
|
179
197
|
|
|
180
|
-
// Step 1: find matching people
|
|
181
198
|
const human = await ctx.ei.getHuman();
|
|
182
|
-
const matches = (human.people ?? []).filter(p =>
|
|
183
|
-
p.name.toLowerCase().includes(personName.toLowerCase())
|
|
184
|
-
);
|
|
185
199
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
name: p.name,
|
|
197
|
-
relationship: p.relationship,
|
|
198
|
-
description: p.description,
|
|
199
|
-
}));
|
|
200
|
-
|
|
201
|
-
const choice = await new Promise<typeof matches[0] | null>((resolve) => {
|
|
202
|
-
ctx.showOverlay((hideOverlay, _hideForEditor) => (
|
|
203
|
-
<PersonPickerOverlay
|
|
204
|
-
title={`Multiple matches for "${personName}"`}
|
|
205
|
-
people={people}
|
|
206
|
-
onSelect={(item) => {
|
|
207
|
-
hideOverlay();
|
|
208
|
-
const found = matches.find(m => m.id === item.id);
|
|
209
|
-
resolve(found ?? null);
|
|
210
|
-
}}
|
|
211
|
-
onDismiss={() => {
|
|
212
|
-
hideOverlay();
|
|
213
|
-
resolve(null);
|
|
214
|
-
}}
|
|
215
|
-
/>
|
|
216
|
-
), ctx.renderer);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
if (!choice) return;
|
|
220
|
-
selectedPerson = choice;
|
|
200
|
+
let selectedPerson: (typeof human.people)[0];
|
|
201
|
+
if (!personName) {
|
|
202
|
+
const linked = (human.people ?? []).find(p =>
|
|
203
|
+
p.identifiers?.some(id => id.type === 'Ei Persona' && id.value === personaId)
|
|
204
|
+
);
|
|
205
|
+
if (!linked) {
|
|
206
|
+
ctx.showNotification(`No person linked to "${personaName}". Try: /p update ${personaName} <personName>`, "error");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
selectedPerson = linked;
|
|
221
210
|
} else {
|
|
222
|
-
|
|
211
|
+
const matches = (human.people ?? []).filter(p =>
|
|
212
|
+
p.name.toLowerCase().includes(personName.toLowerCase())
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
if (matches.length === 0) {
|
|
216
|
+
ctx.showNotification(`No person named "${personName}" in your data`, "error");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (matches.length > 1) {
|
|
221
|
+
const people: PersonPickerItem[] = matches.map(p => ({
|
|
222
|
+
id: p.id,
|
|
223
|
+
name: p.name,
|
|
224
|
+
relationship: p.relationship,
|
|
225
|
+
description: p.description,
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
const choice = await new Promise<typeof matches[0] | null>((resolve) => {
|
|
229
|
+
ctx.showOverlay((hideOverlay, _hideForEditor) => (
|
|
230
|
+
<PersonPickerOverlay
|
|
231
|
+
title={`Multiple matches for "${personName}"`}
|
|
232
|
+
people={people}
|
|
233
|
+
onSelect={(item) => {
|
|
234
|
+
hideOverlay();
|
|
235
|
+
const found = matches.find(m => m.id === item.id);
|
|
236
|
+
resolve(found ?? null);
|
|
237
|
+
}}
|
|
238
|
+
onDismiss={() => {
|
|
239
|
+
hideOverlay();
|
|
240
|
+
resolve(null);
|
|
241
|
+
}}
|
|
242
|
+
/>
|
|
243
|
+
), ctx.renderer);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (!choice) return;
|
|
247
|
+
selectedPerson = choice;
|
|
248
|
+
} else {
|
|
249
|
+
selectedPerson = matches[0];
|
|
250
|
+
}
|
|
223
251
|
}
|
|
224
252
|
|
|
225
253
|
// Step 3: generate preview with loading overlay
|
|
@@ -301,6 +329,15 @@ export const personaCommand: Command = {
|
|
|
301
329
|
traits: previewParsed.traits,
|
|
302
330
|
topics: previewParsed.topics,
|
|
303
331
|
});
|
|
332
|
+
|
|
333
|
+
const existingIdentifiers = selectedPerson.identifiers ?? [];
|
|
334
|
+
const alreadyLinked = existingIdentifiers.some(id => id.type === 'Ei Persona' && id.value === personaId);
|
|
335
|
+
if (!alreadyLinked) {
|
|
336
|
+
const isPrimaryFirst = existingIdentifiers.length === 0;
|
|
337
|
+
const updatedIdentifiers = [...existingIdentifiers, { type: 'Ei Persona', value: personaId, ...(isPrimaryFirst ? { is_primary: true } : {}) }];
|
|
338
|
+
await ctx.ei.upsertPerson({ ...selectedPerson, identifiers: updatedIdentifiers });
|
|
339
|
+
}
|
|
340
|
+
|
|
304
341
|
ctx.showNotification(`Updated ${persona.display_name}`, "info");
|
|
305
342
|
return;
|
|
306
343
|
}
|
|
@@ -19,14 +19,19 @@ function formatTime(timestamp: string): string {
|
|
|
19
19
|
return `${hours}:${minutes}`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function getContent(msg: { content?: string; verbal_response?: string; action_response?: string }): string {
|
|
23
|
+
if (msg.content) return msg.content;
|
|
24
|
+
const parts: string[] = [];
|
|
25
|
+
if (msg.action_response) parts.push(`_${msg.action_response}_`);
|
|
26
|
+
if (msg.verbal_response) parts.push(msg.verbal_response);
|
|
27
|
+
return parts.join('\n\n');
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
function buildMessageText(message: Message): string {
|
|
23
31
|
if (message.silence_reason !== undefined) {
|
|
24
32
|
return `[chose not to respond: ${message.silence_reason}]`;
|
|
25
33
|
}
|
|
26
|
-
|
|
27
|
-
if (message.action_response) parts.push(`_${message.action_response}_`);
|
|
28
|
-
if (message.verbal_response) parts.push(message.verbal_response);
|
|
29
|
-
return parts.join('\n\n');
|
|
34
|
+
return getContent(message);
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
let instanceId = 0;
|
|
@@ -7,6 +7,14 @@ import type { RoomMessage, Quote } from "../../../src/core/types.js";
|
|
|
7
7
|
import { RoomMode } from "../../../src/core/types/enums.js";
|
|
8
8
|
import { insertQuoteMarkers } from "../util/quote-utils.js";
|
|
9
9
|
|
|
10
|
+
function getContent(msg: { content?: string; verbal_response?: string; action_response?: string }): string {
|
|
11
|
+
if (msg.content) return msg.content;
|
|
12
|
+
const parts: string[] = [];
|
|
13
|
+
if (msg.action_response) parts.push(`_${msg.action_response}_`);
|
|
14
|
+
if (msg.verbal_response) parts.push(msg.verbal_response);
|
|
15
|
+
return parts.join('\n\n');
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
interface RoomMessageWithQuotes extends RoomMessage {
|
|
11
19
|
_quotes: Quote[];
|
|
12
20
|
}
|
|
@@ -138,7 +146,7 @@ export function RoomMessageList() {
|
|
|
138
146
|
? ` ⑂${siblingCount}`
|
|
139
147
|
: "";
|
|
140
148
|
const header = `${speakerName} (${formatTime(msg.timestamp)}) [${idx}]${branchIndicator}:`;
|
|
141
|
-
const isSilence = msg.silence_reason !== undefined && !msg
|
|
149
|
+
const isSilence = msg.silence_reason !== undefined && !getContent(msg);
|
|
142
150
|
const isJudge = activeRoom()?.judge_persona_id !== undefined
|
|
143
151
|
&& msg.persona_id === activeRoom()?.judge_persona_id;
|
|
144
152
|
const silenceText = isSilence
|
|
@@ -146,11 +154,8 @@ export function RoomMessageList() {
|
|
|
146
154
|
? `[${speakerName}'s verdict: ${msg.silence_reason ?? ""}]`
|
|
147
155
|
: `[${speakerName} chose not to respond: ${msg.silence_reason ?? ""}]`
|
|
148
156
|
: "";
|
|
149
|
-
const contentParts: string[] = [];
|
|
150
|
-
if (msg.action_response) contentParts.push(`_${msg.action_response}_`);
|
|
151
|
-
if (msg.verbal_response) contentParts.push(msg.verbal_response);
|
|
152
157
|
const msgQuotes = msg._quotes;
|
|
153
|
-
const normalContent = insertQuoteMarkers(
|
|
158
|
+
const normalContent = insertQuoteMarkers(getContent(msg), msgQuotes);
|
|
154
159
|
|
|
155
160
|
return (
|
|
156
161
|
<box flexDirection="column" marginBottom={1}>
|
|
@@ -178,7 +178,7 @@ export const KeyboardProvider: ParentComponent = (props) => {
|
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
// Navigate backward through sent-message history
|
|
181
|
-
|
|
181
|
+
const history = messages().filter(m => m.role === "human").map(m => (m.content ?? m.verbal_response ?? ''));
|
|
182
182
|
if (history.length === 0) return;
|
|
183
183
|
if (historyIndex === -1) {
|
|
184
184
|
savedDraft = textareaRef.plainText;
|
|
@@ -206,7 +206,7 @@ export const KeyboardProvider: ParentComponent = (props) => {
|
|
|
206
206
|
textareaRef.gotoBufferEnd();
|
|
207
207
|
} else {
|
|
208
208
|
historyIndex -= 1;
|
|
209
|
-
|
|
209
|
+
const history = messages().filter(m => m.role === "human").map(m => (m.content ?? m.verbal_response ?? ''));
|
|
210
210
|
const entry = history[history.length - 1 - historyIndex];
|
|
211
211
|
textareaRef.setText(entry);
|
|
212
212
|
textareaRef.gotoBufferEnd(); // cursor at end so next Down continues forward
|
|
@@ -7,6 +7,14 @@ interface ParsedBlock {
|
|
|
7
7
|
chosen: boolean;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
function getContent(m: { content?: string; verbal_response?: string; action_response?: string }): string {
|
|
11
|
+
if (m.content) return m.content;
|
|
12
|
+
const parts: string[] = [];
|
|
13
|
+
if (m.action_response) parts.push(`_${m.action_response}_`);
|
|
14
|
+
if (m.verbal_response) parts.push(m.verbal_response);
|
|
15
|
+
return parts.join('\n\n');
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
export function buildCYPEditorYAML(
|
|
11
19
|
activeNodeId: string,
|
|
12
20
|
messages: RoomMessage[],
|
|
@@ -29,15 +37,12 @@ export function buildCYPEditorYAML(
|
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
const contentLines: string[] = [];
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (m.action_response !== undefined) {
|
|
37
|
-
const indented = m.action_response.split("\n").map((l) => ` ${l}`).join("\n");
|
|
38
|
-
contentLines.push(` action_response: |\n${indented}`);
|
|
40
|
+
const msgContent = getContent(m);
|
|
41
|
+
if (msgContent) {
|
|
42
|
+
const indented = msgContent.split("\n").map((l) => ` ${l}`).join("\n");
|
|
43
|
+
contentLines.push(` content: |\n${indented}`);
|
|
39
44
|
}
|
|
40
|
-
if (m.silence_reason !== undefined &&
|
|
45
|
+
if (m.silence_reason !== undefined && !msgContent) {
|
|
41
46
|
contentLines.push(` silence_reason: "${m.silence_reason}"`);
|
|
42
47
|
}
|
|
43
48
|
if (contentLines.length === 0) {
|