ei-tui 0.6.7 → 0.7.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/cli/mcp.ts +35 -10
- package/src/cli/persona-filter.ts +42 -0
- package/src/cli.ts +18 -6
- package/src/core/handlers/human-extraction.ts +1 -0
- package/src/core/handlers/human-matching.ts +10 -0
- package/src/core/handlers/index.ts +2 -1
- package/src/core/handlers/persona-response.ts +5 -0
- package/src/core/handlers/utils.ts +4 -1
- package/src/core/orchestrators/ceremony.ts +2 -2
- package/src/core/orchestrators/human-extraction.ts +5 -0
- package/src/core/personas/opencode-agent.ts +1 -0
- package/src/core/processor.ts +22 -2
- package/src/core/prompt-context-builder.ts +40 -10
- package/src/core/queue-manager.ts +18 -0
- package/src/core/room-manager.ts +21 -4
- package/src/core/state/personas.ts +2 -2
- package/src/core/state-manager.ts +26 -0
- package/src/core/types/data-items.ts +1 -0
- package/src/core/types/enums.ts +1 -0
- package/src/core/types/integrations.ts +1 -0
- package/src/core/types/rooms.ts +2 -0
- package/src/integrations/claude-code/importer.ts +3 -57
- package/src/integrations/cursor/importer.ts +2 -52
- package/src/integrations/opencode/importer.ts +1 -0
- package/src/prompts/response/sections.ts +1 -1
- package/src/prompts/response/types.ts +1 -0
- package/src/prompts/room/index.ts +2 -2
- package/src/prompts/room/sections.ts +4 -4
- package/src/prompts/room/types.ts +4 -0
- package/tui/src/commands/activate.tsx +7 -6
- package/tui/src/commands/context.tsx +188 -2
- package/tui/src/components/CYPTreeOverlay.tsx +357 -0
- package/tui/src/components/MAPScoreOverlay.tsx +300 -0
- package/tui/src/components/MessageList.tsx +14 -3
- package/tui/src/components/RoomMessageList.tsx +15 -3
- package/tui/src/context/ei.tsx +20 -0
- package/tui/src/util/cyp-tree.ts +62 -0
- package/tui/src/util/yaml-context.ts +87 -1
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
RoomSummary,
|
|
18
18
|
RoomCreationInput,
|
|
19
19
|
} from "./types.js";
|
|
20
|
+
import { RoomMode } from "./types.js";
|
|
20
21
|
import { BUILT_IN_FACT_NAMES } from './constants/built-in-facts.js';
|
|
21
22
|
import type { ThemeDefinition } from './types/entities.js';
|
|
22
23
|
import type { Storage } from "../storage/interface.js";
|
|
@@ -70,6 +71,7 @@ export class StateManager {
|
|
|
70
71
|
this.migrateProviderModel();
|
|
71
72
|
this.migrateRoomMessageContent();
|
|
72
73
|
this.migrateThemes();
|
|
74
|
+
this.migrateFfaParentIds();
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
private migrateRoomMessageContent(): void {
|
|
@@ -576,6 +578,30 @@ export class StateManager {
|
|
|
576
578
|
this.humanState.set(human);
|
|
577
579
|
}
|
|
578
580
|
|
|
581
|
+
private migrateFfaParentIds(): void {
|
|
582
|
+
const rooms = this.roomState.getAll(true);
|
|
583
|
+
let migratedCount = 0;
|
|
584
|
+
|
|
585
|
+
for (const room of rooms) {
|
|
586
|
+
if (room.mode !== RoomMode.FreeForAll) continue;
|
|
587
|
+
const rootMsg = room.messages.find(m => m.parent_id === null);
|
|
588
|
+
if (!rootMsg) continue;
|
|
589
|
+
|
|
590
|
+
for (const msg of room.messages) {
|
|
591
|
+
if (msg.role !== "human") continue;
|
|
592
|
+
if (msg.id === rootMsg.id) continue;
|
|
593
|
+
if (msg.parent_id === rootMsg.id) continue;
|
|
594
|
+
msg.parent_id = rootMsg.id;
|
|
595
|
+
migratedCount++;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (migratedCount > 0) {
|
|
600
|
+
this.scheduleSave();
|
|
601
|
+
console.log(`[StateManager] Migrated ${migratedCount} FFA human messages to root parent_id`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
579
605
|
getHuman(): HumanEntity {
|
|
580
606
|
return this.humanState.get();
|
|
581
607
|
}
|
|
@@ -15,6 +15,7 @@ export interface DataItemBase {
|
|
|
15
15
|
learned_by?: string; // Persona ID that originally learned this item (stable UUID)
|
|
16
16
|
last_changed_by?: string; // Persona ID that most recently updated this item (stable UUID)
|
|
17
17
|
interested_personas?: string[]; // Persona IDs that have extracted/touched this item (accumulated)
|
|
18
|
+
sources?: string[]; // Namespaced source identifiers — where items were learned from. Format: "provider:id" (e.g., "opencode:ses_abc123", "cursor:composerId"). Grow-only union.
|
|
18
19
|
persona_groups?: string[];
|
|
19
20
|
embedding?: number[];
|
|
20
21
|
rewrite_checked?: boolean; // True after rewrite scan finds no changes. Cleared automatically when extraction upserts a fresh item.
|
package/src/core/types/enums.ts
CHANGED
|
@@ -38,6 +38,7 @@ export enum LLMNextStep {
|
|
|
38
38
|
HandleHeartbeatCheck = "handleHeartbeatCheck",
|
|
39
39
|
HandleEiHeartbeat = "handleEiHeartbeat",
|
|
40
40
|
HandleOneShot = "handleOneShot",
|
|
41
|
+
HandleOneShotJSON = "handleOneShotJSON",
|
|
41
42
|
// Tool calling continuation (second LLM call after tool execution, may loop for more tool calls).
|
|
42
43
|
// data.toolHistory: serialized LLMHistoryMessage[] (assistant + tool result messages)
|
|
43
44
|
// data.toolCallCounts: serialized Map entries [[name, count], ...] carrying per-tool call counts
|
|
@@ -102,6 +102,7 @@ export interface Ei_Interface {
|
|
|
102
102
|
onError?: (error: EiError) => void;
|
|
103
103
|
onStateImported?: () => void;
|
|
104
104
|
onOneShotReturned?: (guid: string, content: string) => void;
|
|
105
|
+
onOneShotJSONReturned?: (guid: string, parsed: unknown) => void;
|
|
105
106
|
onContextBoundaryChanged?: (personaId: string) => void;
|
|
106
107
|
onSaveAndExitStart?: () => void;
|
|
107
108
|
onSaveAndExitFinish?: () => void;
|
package/src/core/types/rooms.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface RoomEntity {
|
|
|
38
38
|
last_updated: string;
|
|
39
39
|
last_activity: string;
|
|
40
40
|
capture_used?: boolean;
|
|
41
|
+
context_window_hours?: number; // FFA only; falls back to human.settings.default_context_window_hours
|
|
42
|
+
context_boundary?: string; // FFA only; ISO timestamp; same semantics as persona context_boundary
|
|
41
43
|
messages: RoomMessage[];
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { StateManager } from "../../core/state-manager.js";
|
|
2
|
-
import type { Ei_Interface,
|
|
2
|
+
import type { Ei_Interface, Message, ContextStatus, PersonaEntity, PersonaTrait } from "../../core/types.js";
|
|
3
3
|
import { DEFAULT_SEED_TRAITS } from "../../core/constants/seed-traits.js";
|
|
4
4
|
import type { IClaudeCodeReader, ClaudeCodeSession, ClaudeCodeMessage } from "./types.js";
|
|
5
5
|
import {
|
|
6
6
|
CLAUDE_CODE_PERSONA_NAME,
|
|
7
|
-
CLAUDE_CODE_TOPIC_GROUPS,
|
|
8
7
|
MIN_SESSION_AGE_MS,
|
|
9
8
|
} from "./types.js";
|
|
10
9
|
import { ClaudeCodeReader } from "./reader.js";
|
|
@@ -20,8 +19,6 @@ import { isProcessRunning } from "../process-check.js";
|
|
|
20
19
|
|
|
21
20
|
export interface ClaudeCodeImportResult {
|
|
22
21
|
sessionsProcessed: number;
|
|
23
|
-
topicsCreated: number;
|
|
24
|
-
topicsUpdated: number;
|
|
25
22
|
messagesImported: number;
|
|
26
23
|
personaCreated: boolean;
|
|
27
24
|
extractionScansQueued: number;
|
|
@@ -109,47 +106,6 @@ function ensureClaudeCodePersona(
|
|
|
109
106
|
return persona;
|
|
110
107
|
}
|
|
111
108
|
|
|
112
|
-
// =============================================================================
|
|
113
|
-
// Topic Management
|
|
114
|
-
// =============================================================================
|
|
115
|
-
|
|
116
|
-
function ensureSessionTopic(
|
|
117
|
-
session: ClaudeCodeSession,
|
|
118
|
-
stateManager: StateManager
|
|
119
|
-
): "created" | "updated" | "unchanged" {
|
|
120
|
-
const human = stateManager.getHuman();
|
|
121
|
-
const existingTopic = human.topics.find((t) => t.id === session.id);
|
|
122
|
-
|
|
123
|
-
if (existingTopic) {
|
|
124
|
-
if (existingTopic.name !== session.title) {
|
|
125
|
-
const updatedTopic: Topic = {
|
|
126
|
-
...existingTopic,
|
|
127
|
-
name: session.title,
|
|
128
|
-
last_updated: new Date().toISOString(),
|
|
129
|
-
};
|
|
130
|
-
stateManager.human_topic_upsert(updatedTopic);
|
|
131
|
-
return "updated";
|
|
132
|
-
}
|
|
133
|
-
return "unchanged";
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const newTopic: Topic = {
|
|
137
|
-
id: session.id,
|
|
138
|
-
name: session.title,
|
|
139
|
-
description: `Claude Code session in ${session.cwd}`,
|
|
140
|
-
sentiment: 0,
|
|
141
|
-
exposure_current: 0.5,
|
|
142
|
-
exposure_desired: 0.3,
|
|
143
|
-
persona_groups: CLAUDE_CODE_TOPIC_GROUPS,
|
|
144
|
-
learned_by: stateManager.persona_getByName(CLAUDE_CODE_PERSONA_NAME)?.id ?? undefined,
|
|
145
|
-
last_updated: new Date().toISOString(),
|
|
146
|
-
learned_on: new Date().toISOString(),
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
stateManager.human_topic_upsert(newTopic);
|
|
150
|
-
return "created";
|
|
151
|
-
}
|
|
152
|
-
|
|
153
109
|
// =============================================================================
|
|
154
110
|
// State Helpers
|
|
155
111
|
// =============================================================================
|
|
@@ -205,26 +161,15 @@ export async function importClaudeCodeSessions(
|
|
|
205
161
|
|
|
206
162
|
const result: ClaudeCodeImportResult = {
|
|
207
163
|
sessionsProcessed: 0,
|
|
208
|
-
topicsCreated: 0,
|
|
209
|
-
topicsUpdated: 0,
|
|
210
164
|
messagesImported: 0,
|
|
211
165
|
personaCreated: false,
|
|
212
166
|
extractionScansQueued: 0,
|
|
213
167
|
};
|
|
214
168
|
|
|
215
|
-
// ─── Step 1:
|
|
169
|
+
// ─── Step 1: Get all sessions ─────────────────────────────────────────
|
|
216
170
|
const allSessions = await reader.getSessions();
|
|
217
171
|
|
|
218
|
-
for (const session of allSessions) {
|
|
219
|
-
const topicResult = ensureSessionTopic(session, stateManager);
|
|
220
|
-
if (topicResult === "created") result.topicsCreated++;
|
|
221
|
-
else if (topicResult === "updated") result.topicsUpdated++;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
172
|
if (signal?.aborted) return result;
|
|
225
|
-
if (result.topicsCreated > 0 || result.topicsUpdated > 0) {
|
|
226
|
-
eiInterface?.onHumanUpdated?.();
|
|
227
|
-
}
|
|
228
173
|
|
|
229
174
|
// ─── Step 2: Find next unprocessed session ────────────────────────────
|
|
230
175
|
const human = stateManager.getHuman();
|
|
@@ -323,6 +268,7 @@ export async function importClaudeCodeSessions(
|
|
|
323
268
|
personaDisplayName: persona.display_name,
|
|
324
269
|
messages_context: contextMsgs,
|
|
325
270
|
messages_analyze: toAnalyze,
|
|
271
|
+
sources: [`claudecode:${targetSession.id}`],
|
|
326
272
|
};
|
|
327
273
|
|
|
328
274
|
const ccSettings = stateManager.getHuman().settings?.claudeCode;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { StateManager } from "../../core/state-manager.js";
|
|
2
|
-
import type { Ei_Interface,
|
|
2
|
+
import type { Ei_Interface, Message, ContextStatus, PersonaEntity, PersonaTrait } from "../../core/types.js";
|
|
3
3
|
import { DEFAULT_SEED_TRAITS } from "../../core/constants/seed-traits.js";
|
|
4
4
|
import type { ICursorReader, CursorSession, CursorMessage } from "./types.js";
|
|
5
5
|
import {
|
|
6
6
|
CURSOR_PERSONA_NAME,
|
|
7
|
-
CURSOR_TOPIC_GROUPS,
|
|
8
7
|
MIN_SESSION_AGE_MS,
|
|
9
8
|
} from "./types.js";
|
|
10
9
|
import { CursorReader } from "./reader.js";
|
|
@@ -16,8 +15,6 @@ import {
|
|
|
16
15
|
|
|
17
16
|
export interface CursorImportResult {
|
|
18
17
|
sessionsProcessed: number;
|
|
19
|
-
topicsCreated: number;
|
|
20
|
-
topicsUpdated: number;
|
|
21
18
|
messagesImported: number;
|
|
22
19
|
personaCreated: boolean;
|
|
23
20
|
extractionScansQueued: number;
|
|
@@ -97,43 +94,6 @@ function ensureCursorPersona(
|
|
|
97
94
|
return persona;
|
|
98
95
|
}
|
|
99
96
|
|
|
100
|
-
function ensureSessionTopic(
|
|
101
|
-
session: CursorSession,
|
|
102
|
-
stateManager: StateManager
|
|
103
|
-
): "created" | "updated" | "unchanged" {
|
|
104
|
-
const human = stateManager.getHuman();
|
|
105
|
-
const existingTopic = human.topics.find((t) => t.id === session.id);
|
|
106
|
-
|
|
107
|
-
if (existingTopic) {
|
|
108
|
-
if (existingTopic.name !== session.name) {
|
|
109
|
-
const updatedTopic: Topic = {
|
|
110
|
-
...existingTopic,
|
|
111
|
-
name: session.name,
|
|
112
|
-
last_updated: new Date().toISOString(),
|
|
113
|
-
};
|
|
114
|
-
stateManager.human_topic_upsert(updatedTopic);
|
|
115
|
-
return "updated";
|
|
116
|
-
}
|
|
117
|
-
return "unchanged";
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const newTopic: Topic = {
|
|
121
|
-
id: session.id,
|
|
122
|
-
name: session.name,
|
|
123
|
-
description: `Cursor session in ${session.workspacePath}`,
|
|
124
|
-
sentiment: 0,
|
|
125
|
-
exposure_current: 0.5,
|
|
126
|
-
exposure_desired: 0.3,
|
|
127
|
-
persona_groups: CURSOR_TOPIC_GROUPS,
|
|
128
|
-
learned_by: stateManager.persona_getByName(CURSOR_PERSONA_NAME)?.id ?? undefined,
|
|
129
|
-
last_updated: new Date().toISOString(),
|
|
130
|
-
learned_on: new Date().toISOString(),
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
stateManager.human_topic_upsert(newTopic);
|
|
134
|
-
return "created";
|
|
135
|
-
}
|
|
136
|
-
|
|
137
97
|
function updateProcessedState(
|
|
138
98
|
stateManager: StateManager,
|
|
139
99
|
session: CursorSession
|
|
@@ -173,8 +133,6 @@ export async function importCursorSessions(
|
|
|
173
133
|
|
|
174
134
|
const result: CursorImportResult = {
|
|
175
135
|
sessionsProcessed: 0,
|
|
176
|
-
topicsCreated: 0,
|
|
177
|
-
topicsUpdated: 0,
|
|
178
136
|
messagesImported: 0,
|
|
179
137
|
personaCreated: false,
|
|
180
138
|
extractionScansQueued: 0,
|
|
@@ -182,16 +140,7 @@ export async function importCursorSessions(
|
|
|
182
140
|
|
|
183
141
|
const allSessions = await reader.getSessions();
|
|
184
142
|
|
|
185
|
-
for (const session of allSessions) {
|
|
186
|
-
const topicResult = ensureSessionTopic(session, stateManager);
|
|
187
|
-
if (topicResult === "created") result.topicsCreated++;
|
|
188
|
-
else if (topicResult === "updated") result.topicsUpdated++;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
143
|
if (signal?.aborted) return result;
|
|
192
|
-
if (result.topicsCreated > 0 || result.topicsUpdated > 0) {
|
|
193
|
-
eiInterface?.onHumanUpdated?.();
|
|
194
|
-
}
|
|
195
144
|
|
|
196
145
|
const human = stateManager.getHuman();
|
|
197
146
|
const processedSessions = human.settings?.cursor?.processed_sessions ?? {};
|
|
@@ -278,6 +227,7 @@ export async function importCursorSessions(
|
|
|
278
227
|
personaDisplayName: persona.display_name,
|
|
279
228
|
messages_context: contextMsgs,
|
|
280
229
|
messages_analyze: toAnalyze,
|
|
230
|
+
sources: [`cursor:${targetSession.id}`],
|
|
281
231
|
};
|
|
282
232
|
|
|
283
233
|
queueAllScans(context, stateManager, { external_filter: "only" });
|
|
@@ -277,7 +277,7 @@ export function buildQuotesSection(quotes: Quote[], human: ResponsePromptData["h
|
|
|
277
277
|
const idToName = new Map(allDataItems.map(item => [item.id, item.name]));
|
|
278
278
|
|
|
279
279
|
const formatted = quotes.map(q => {
|
|
280
|
-
const speaker = q.speaker === "human" ?
|
|
280
|
+
const speaker = q.speaker === "human" ? human.name : q.speaker;
|
|
281
281
|
const date = formatDate(q.timestamp);
|
|
282
282
|
const linkedNames = q.data_item_ids
|
|
283
283
|
.map(id => idToName.get(id))
|
|
@@ -97,10 +97,10 @@ There is no objectively correct answer. Pick the response you find most interest
|
|
|
97
97
|
**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.`;
|
|
98
98
|
|
|
99
99
|
const contextSection = context.length > 0
|
|
100
|
-
? buildRoomHistorySection(context)
|
|
100
|
+
? buildRoomHistorySection(context, data.human.name)
|
|
101
101
|
: "";
|
|
102
102
|
|
|
103
|
-
const candidatesSection = buildJudgeCandidatesSection(candidates);
|
|
103
|
+
const candidatesSection = buildJudgeCandidatesSection(candidates, data.human.name);
|
|
104
104
|
const decisionSection = buildJudgeDecisionFormatSection();
|
|
105
105
|
const currentTime = formatCurrentTime();
|
|
106
106
|
|
|
@@ -32,11 +32,11 @@ export function buildRoomParticipantsSection(participants: RoomParticipantIdenti
|
|
|
32
32
|
return `## Others in the Room\n\n${lines.join("\n\n")}`;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function buildRoomHistorySection(history: RoomHistoryMessage[]): string {
|
|
35
|
+
export function buildRoomHistorySection(history: RoomHistoryMessage[], humanName: string): string {
|
|
36
36
|
if (history.length === 0) return "";
|
|
37
37
|
|
|
38
38
|
const lines = history.map(msg => {
|
|
39
|
-
const speaker = msg.speaker_id === "human" ?
|
|
39
|
+
const speaker = msg.speaker_id === "human" ? humanName : msg.speaker_name;
|
|
40
40
|
if (msg.silence_reason) {
|
|
41
41
|
return `**${speaker}**: *[chose not to respond: ${msg.silence_reason}]*`;
|
|
42
42
|
}
|
|
@@ -116,9 +116,9 @@ ${lines.join("\n\n")}
|
|
|
116
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.`;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
export function buildJudgeCandidatesSection(candidates: RoomJudgeCandidate[]): string {
|
|
119
|
+
export function buildJudgeCandidatesSection(candidates: RoomJudgeCandidate[], humanName: string): string {
|
|
120
120
|
const lines = candidates.map((c, i) => {
|
|
121
|
-
const speaker = c.speaker_id === "human" ?
|
|
121
|
+
const speaker = c.speaker_id === "human" ? humanName : c.speaker_name;
|
|
122
122
|
const content = c.silence_reason
|
|
123
123
|
? `*[chose not to respond: ${c.silence_reason}]*`
|
|
124
124
|
: [c.verbal_response, c.action_response ? `*${c.action_response}*` : ""].filter(Boolean).join(" ");
|
|
@@ -39,6 +39,7 @@ export interface RoomResponsePromptData {
|
|
|
39
39
|
};
|
|
40
40
|
other_participants: RoomParticipantIdentity[];
|
|
41
41
|
human: {
|
|
42
|
+
name: string;
|
|
42
43
|
facts: Fact[];
|
|
43
44
|
topics: Topic[];
|
|
44
45
|
people: Person[];
|
|
@@ -72,6 +73,9 @@ export interface RoomJudgePromptData {
|
|
|
72
73
|
long_description?: string;
|
|
73
74
|
traits: PersonaTrait[];
|
|
74
75
|
};
|
|
76
|
+
human: {
|
|
77
|
+
name: string;
|
|
78
|
+
};
|
|
75
79
|
context: RoomHistoryMessage[];
|
|
76
80
|
candidates: RoomJudgeCandidate[];
|
|
77
81
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Command } from "./registry";
|
|
2
2
|
import { RoomMode } from "../../../src/core/types/enums.js";
|
|
3
3
|
import { openCYPEditor } from "../util/cyp-editor.js";
|
|
4
|
+
import { buildCYPTree } from "../util/cyp-tree.js";
|
|
4
5
|
|
|
5
6
|
export const activateCommand: Command = {
|
|
6
7
|
name: "activate",
|
|
@@ -55,18 +56,18 @@ export const activateCommand: Command = {
|
|
|
55
56
|
|
|
56
57
|
const num = parseInt(args[0], 10);
|
|
57
58
|
if (isNaN(num) || num < 1) {
|
|
58
|
-
ctx.showNotification("Usage: /activate <num> (1-based
|
|
59
|
+
ctx.showNotification("Usage: /activate <num> (1-based BFS tree number)", "error");
|
|
59
60
|
return;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
const messages = ctx.ei.roomMessages();
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
const { numToId } = buildCYPTree(messages);
|
|
65
|
+
const targetId = numToId.get(num);
|
|
66
|
+
if (!targetId) {
|
|
67
|
+
ctx.showNotification(`No message at tree position ${num} (tree has ${numToId.size} nodes)`, "error");
|
|
66
68
|
return;
|
|
67
69
|
}
|
|
68
|
-
|
|
69
|
-
await ctx.ei.selectCYPBranch(target.id);
|
|
70
|
+
await ctx.ei.selectCYPBranch(targetId);
|
|
70
71
|
|
|
71
72
|
const freshRoom = ctx.ei.getRoom(roomId);
|
|
72
73
|
const newActiveNodeId = freshRoom?.active_node_id;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { Command } from "./registry.js";
|
|
2
2
|
import { spawnEditor } from "../util/editor.js";
|
|
3
|
-
import { contextToYAML, contextFromYAML } from "../util/yaml-serializers.js";
|
|
3
|
+
import { contextToYAML, contextFromYAML, ffaContextToYAML, ffaContextFromYAML } from "../util/yaml-serializers.js";
|
|
4
4
|
import { logger } from "../util/logger.js";
|
|
5
5
|
import { ConfirmOverlay } from "../components/ConfirmOverlay.js";
|
|
6
|
+
import { CYPTreeOverlay } from "../components/CYPTreeOverlay.js";
|
|
7
|
+
import { MAPScoreOverlay } from "../components/MAPScoreOverlay.js";
|
|
8
|
+
import { RoomMode } from "../../../src/core/types/enums.js";
|
|
6
9
|
|
|
7
10
|
export const contextCommand: Command = {
|
|
8
11
|
name: "context",
|
|
@@ -12,8 +15,191 @@ export const contextCommand: Command = {
|
|
|
12
15
|
|
|
13
16
|
async execute(_args, ctx) {
|
|
14
17
|
const personaId = ctx.ei.activePersonaId();
|
|
18
|
+
const roomId = ctx.ei.activeRoomId();
|
|
19
|
+
|
|
20
|
+
if (roomId) {
|
|
21
|
+
const room = ctx.ei.getRoom(roomId);
|
|
22
|
+
if (!room) {
|
|
23
|
+
ctx.showNotification("Room not found", "warn");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (room.mode === RoomMode.ChooseYourPath) {
|
|
28
|
+
const activeNodeId = room.active_node_id;
|
|
29
|
+
if (!activeNodeId) {
|
|
30
|
+
ctx.showNotification("No active node in room", "warn");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
ctx.showOverlay((hideOverlay) => (
|
|
34
|
+
<CYPTreeOverlay
|
|
35
|
+
roomId={roomId}
|
|
36
|
+
roomName={room.display_name}
|
|
37
|
+
messages={ctx.ei.roomMessages()}
|
|
38
|
+
activeNodeId={activeNodeId}
|
|
39
|
+
activeRoomPath={ctx.ei.roomActivePath()}
|
|
40
|
+
personas={ctx.ei.personas()}
|
|
41
|
+
onSelectBranch={(msgId) => ctx.ei.selectCYPBranch(msgId)}
|
|
42
|
+
onDismiss={hideOverlay}
|
|
43
|
+
/>
|
|
44
|
+
), ctx.renderer);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (room.mode === RoomMode.FreeForAll) {
|
|
49
|
+
const allMessages = ctx.ei.roomMessages();
|
|
50
|
+
if (allMessages.length === 0) {
|
|
51
|
+
ctx.showNotification("No messages to edit", "info");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const personas = ctx.ei.personas();
|
|
56
|
+
const speakerMap = new Map(personas.map((p) => [p.id, p.display_name]));
|
|
57
|
+
|
|
58
|
+
const originalStatus = new Map(allMessages.map((m) => [m.id, m.context_status]));
|
|
59
|
+
|
|
60
|
+
let yamlContent = ffaContextToYAML(allMessages, speakerMap);
|
|
61
|
+
let editorIteration = 0;
|
|
62
|
+
|
|
63
|
+
while (true) {
|
|
64
|
+
editorIteration++;
|
|
65
|
+
logger.debug("[context] ffa starting editor iteration", { iteration: editorIteration });
|
|
66
|
+
|
|
67
|
+
const result = await spawnEditor({
|
|
68
|
+
initialContent: yamlContent,
|
|
69
|
+
filename: "ffa-context.yaml",
|
|
70
|
+
renderer: ctx.renderer,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
logger.debug("[context] ffa editor returned", {
|
|
74
|
+
iteration: editorIteration,
|
|
75
|
+
aborted: result.aborted,
|
|
76
|
+
success: result.success,
|
|
77
|
+
hasContent: result.content !== null,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (result.aborted) {
|
|
81
|
+
ctx.showNotification("Editor cancelled", "info");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!result.success) {
|
|
86
|
+
ctx.showNotification("Editor failed to open", "error");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (result.content === null) {
|
|
91
|
+
ctx.showNotification("No changes made", "info");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const parsed = ffaContextFromYAML(result.content);
|
|
97
|
+
|
|
98
|
+
if (parsed.deletedMessageIds.length > 0) {
|
|
99
|
+
const count = parsed.deletedMessageIds.length;
|
|
100
|
+
const hasImplicit = parsed.implicitDeleteCount > 0;
|
|
101
|
+
const confirmed = await new Promise<boolean>((resolve) => {
|
|
102
|
+
ctx.showOverlay((hideOverlay) => (
|
|
103
|
+
<ConfirmOverlay
|
|
104
|
+
message={`Delete ${count} message${count === 1 ? "" : "s"}?${hasImplicit ? `\n(includes ${parsed.implicitDeleteCount} persona response${parsed.implicitDeleteCount === 1 ? "" : "s"})` : ""}`}
|
|
105
|
+
onConfirm={() => { hideOverlay(); resolve(true); }}
|
|
106
|
+
onCancel={() => { hideOverlay(); resolve(false); }}
|
|
107
|
+
/>
|
|
108
|
+
), ctx.renderer);
|
|
109
|
+
});
|
|
110
|
+
if (!confirmed) {
|
|
111
|
+
ctx.showNotification("Delete cancelled", "info");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
await ctx.ei.deleteRoomMessages(roomId, parsed.deletedMessageIds);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const msg of parsed.messages) {
|
|
118
|
+
const orig = originalStatus.get(msg.id);
|
|
119
|
+
if (orig !== undefined && orig !== msg.context_status) {
|
|
120
|
+
await ctx.ei.setRoomMessageContextStatus(roomId, msg.id, msg.context_status);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const deleteCount = parsed.deletedMessageIds.length;
|
|
125
|
+
const notification =
|
|
126
|
+
deleteCount > 0
|
|
127
|
+
? `Context updated (${deleteCount} message${deleteCount === 1 ? "" : "s"} deleted)`
|
|
128
|
+
: "Context updated";
|
|
129
|
+
|
|
130
|
+
ctx.showNotification(notification, "info");
|
|
131
|
+
return;
|
|
132
|
+
} catch (parseError) {
|
|
133
|
+
const errorMsg = parseError instanceof Error ? parseError.message : String(parseError);
|
|
134
|
+
logger.debug("[context] ffa YAML parse error, prompting for re-edit", {
|
|
135
|
+
iteration: editorIteration,
|
|
136
|
+
error: errorMsg,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const shouldReEdit = await new Promise<boolean>((resolve) => {
|
|
140
|
+
ctx.showOverlay((hideOverlay, hideForEditor) => (
|
|
141
|
+
<ConfirmOverlay
|
|
142
|
+
message={`YAML parse error:\n${errorMsg}\n\nRe-edit?`}
|
|
143
|
+
onConfirm={() => {
|
|
144
|
+
logger.debug("[context] ffa user confirmed re-edit");
|
|
145
|
+
hideForEditor();
|
|
146
|
+
resolve(true);
|
|
147
|
+
}}
|
|
148
|
+
onCancel={() => {
|
|
149
|
+
logger.debug("[context] ffa user cancelled re-edit");
|
|
150
|
+
hideOverlay();
|
|
151
|
+
resolve(false);
|
|
152
|
+
}}
|
|
153
|
+
/>
|
|
154
|
+
), ctx.renderer);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
logger.debug("[context] ffa shouldReEdit", { shouldReEdit, iteration: editorIteration });
|
|
158
|
+
|
|
159
|
+
if (shouldReEdit) {
|
|
160
|
+
yamlContent = result.content;
|
|
161
|
+
logger.debug("[context] ffa continuing to next iteration");
|
|
162
|
+
continue;
|
|
163
|
+
} else {
|
|
164
|
+
ctx.showNotification("Changes discarded", "info");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (room.mode === RoomMode.MessagesAgainstPersona) {
|
|
172
|
+
if (!room.judge_persona_id) {
|
|
173
|
+
ctx.showNotification("No judge configured for this room", "warn");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const human = await ctx.ei.getHuman();
|
|
177
|
+
const humanName =
|
|
178
|
+
human.settings?.name_display ||
|
|
179
|
+
human.facts?.find((f) => f.name === "Nickname/Preferred Name")?.description ||
|
|
180
|
+
"You";
|
|
181
|
+
ctx.showOverlay((hideOverlay) => (
|
|
182
|
+
<MAPScoreOverlay
|
|
183
|
+
roomId={roomId}
|
|
184
|
+
roomName={room.display_name}
|
|
185
|
+
messages={ctx.ei.roomMessages()}
|
|
186
|
+
activeNodeId={room.active_node_id ?? ""}
|
|
187
|
+
activeRoomPath={ctx.ei.roomActivePath()}
|
|
188
|
+
personas={ctx.ei.personas()}
|
|
189
|
+
judgePersonaId={room.judge_persona_id!}
|
|
190
|
+
humanName={humanName}
|
|
191
|
+
onDismiss={hideOverlay}
|
|
192
|
+
/>
|
|
193
|
+
), ctx.renderer);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
ctx.showNotification("Unknown room mode", "warn");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
15
201
|
if (!personaId) {
|
|
16
|
-
ctx.showNotification("No active
|
|
202
|
+
ctx.showNotification("No active chat", "warn");
|
|
17
203
|
return;
|
|
18
204
|
}
|
|
19
205
|
|