ei-tui 0.4.3 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/package.json +1 -1
- package/src/cli/README.md +17 -12
- package/src/cli/commands/personas.ts +12 -0
- package/src/cli/mcp.ts +2 -2
- package/src/cli/retrieval.ts +86 -8
- package/src/cli.ts +8 -5
- package/src/core/constants/seed-traits.ts +29 -0
- package/src/core/context-utils.ts +1 -0
- package/src/core/handlers/human-matching.ts +86 -56
- package/src/core/handlers/index.ts +5 -0
- package/src/core/handlers/persona-preview.ts +7 -0
- package/src/core/handlers/persona-topics.ts +3 -2
- package/src/core/handlers/rooms.ts +176 -0
- package/src/core/handlers/utils.ts +55 -3
- package/src/core/heartbeat-manager.ts +3 -1
- package/src/core/llm-client.ts +1 -1
- package/src/core/message-manager.ts +10 -8
- package/src/core/orchestrators/human-extraction.ts +15 -2
- package/src/core/orchestrators/index.ts +1 -0
- package/src/core/orchestrators/persona-generation.ts +4 -0
- package/src/core/orchestrators/persona-topics.ts +2 -1
- package/src/core/orchestrators/room-extraction.ts +318 -0
- package/src/core/persona-manager.ts +16 -5
- package/src/core/personas/opencode-agent.ts +12 -2
- package/src/core/processor.ts +520 -4
- package/src/core/prompt-context-builder.ts +89 -5
- package/src/core/queue-processor.ts +68 -8
- package/src/core/room-manager.ts +408 -0
- package/src/core/state/index.ts +1 -0
- package/src/core/state/personas.ts +12 -2
- package/src/core/state/queue.ts +2 -2
- package/src/core/state/rooms.ts +182 -0
- package/src/core/state-manager.ts +124 -2
- package/src/core/tool-manager.ts +1 -1
- package/src/core/tools/index.ts +15 -0
- package/src/core/types/data-items.ts +3 -1
- package/src/core/types/enums.ts +11 -0
- package/src/core/types/integrations.ts +10 -2
- package/src/core/types/llm.ts +3 -0
- package/src/core/types/rooms.ts +59 -0
- package/src/core/types.ts +1 -0
- package/src/core/utils/decay.ts +14 -8
- package/src/core/utils/exposure.ts +14 -0
- package/src/integrations/claude-code/importer.ts +23 -10
- package/src/integrations/cursor/importer.ts +22 -10
- package/src/integrations/opencode/importer.ts +30 -13
- package/src/prompts/ceremony/dedup.ts +2 -2
- package/src/prompts/generation/from-person.ts +85 -0
- package/src/prompts/generation/index.ts +2 -0
- package/src/prompts/generation/persona.ts +14 -10
- package/src/prompts/generation/seeds.ts +4 -29
- package/src/prompts/generation/types.ts +13 -0
- package/src/prompts/heartbeat/check.ts +1 -1
- package/src/prompts/heartbeat/ei.ts +4 -4
- package/src/prompts/heartbeat/types.ts +1 -0
- package/src/prompts/index.ts +15 -0
- package/src/prompts/message-utils.ts +2 -2
- package/src/prompts/persona/topics-match.ts +7 -6
- package/src/prompts/persona/topics-update.ts +8 -11
- package/src/prompts/persona/types.ts +2 -1
- package/src/prompts/response/index.ts +1 -1
- package/src/prompts/response/sections.ts +20 -8
- package/src/prompts/response/types.ts +6 -0
- package/src/prompts/room/index.ts +115 -0
- package/src/prompts/room/sections.ts +150 -0
- package/src/prompts/room/types.ts +93 -0
- package/tui/README.md +20 -0
- package/tui/src/app.tsx +3 -2
- package/tui/src/commands/activate.tsx +98 -0
- package/tui/src/commands/archive.tsx +54 -25
- package/tui/src/commands/capture.tsx +50 -0
- package/tui/src/commands/dedupe.tsx +2 -7
- package/tui/src/commands/delete.tsx +48 -0
- package/tui/src/commands/details.tsx +7 -0
- package/tui/src/commands/persona.tsx +271 -9
- package/tui/src/commands/room.tsx +261 -0
- package/tui/src/commands/silence.tsx +29 -0
- package/tui/src/components/ArchivedItemsOverlay.tsx +144 -0
- package/tui/src/components/ConfirmOverlay.tsx +6 -0
- package/tui/src/components/ConflictOverlay.tsx +6 -0
- package/tui/src/components/HelpOverlay.tsx +6 -1
- package/tui/src/components/LoadingOverlay.tsx +51 -0
- package/tui/src/components/MessageList.tsx +1 -18
- package/tui/src/components/PersonPickerOverlay.tsx +121 -0
- package/tui/src/components/PersonaListOverlay.tsx +6 -1
- package/tui/src/components/PromptInput.tsx +141 -8
- package/tui/src/components/ProviderListOverlay.tsx +5 -1
- package/tui/src/components/QuotesOverlay.tsx +5 -1
- package/tui/src/components/RoomMessageList.tsx +179 -0
- package/tui/src/components/Sidebar.tsx +54 -2
- package/tui/src/components/StatusBar.tsx +99 -8
- package/tui/src/components/ToolkitListOverlay.tsx +5 -1
- package/tui/src/components/WelcomeOverlay.tsx +6 -0
- package/tui/src/context/ei.tsx +252 -1
- package/tui/src/context/keyboard.tsx +48 -12
- package/tui/src/util/cyp-editor.tsx +152 -0
- package/tui/src/util/quote-utils.ts +19 -0
- package/tui/src/util/room-editor.tsx +164 -0
- package/tui/src/util/room-logic.ts +8 -0
- package/tui/src/util/room-parser.ts +70 -0
- package/tui/src/util/yaml-serializers.ts +151 -0
|
@@ -1,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 ToolkitListItem {
|
|
6
7
|
id: string;
|
|
@@ -17,6 +18,9 @@ interface ToolkitListOverlayProps {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export function ToolkitListOverlay(props: ToolkitListOverlayProps) {
|
|
21
|
+
const { setOverlayActive } = useKeyboardNav();
|
|
22
|
+
onMount(() => setOverlayActive(true));
|
|
23
|
+
onCleanup(() => setOverlayActive(false));
|
|
20
24
|
const [selectedIndex, setSelectedIndex] = createSignal(0);
|
|
21
25
|
const [filterText, setFilterText] = createSignal("");
|
|
22
26
|
const [filterMode, setFilterMode] = createSignal(false);
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { useKeyboard } from "@opentui/solid";
|
|
2
|
+
import { onMount, onCleanup } from "solid-js";
|
|
3
|
+
import { useKeyboardNav } from "../context/keyboard.js";
|
|
2
4
|
|
|
3
5
|
interface WelcomeOverlayProps {
|
|
4
6
|
onDismiss: () => void;
|
|
5
7
|
}
|
|
6
8
|
|
|
7
9
|
export function WelcomeOverlay(props: WelcomeOverlayProps) {
|
|
10
|
+
const { setOverlayActive } = useKeyboardNav();
|
|
11
|
+
onMount(() => setOverlayActive(true));
|
|
12
|
+
onCleanup(() => setOverlayActive(false));
|
|
13
|
+
|
|
8
14
|
useKeyboard((event) => {
|
|
9
15
|
event.preventDefault();
|
|
10
16
|
props.onDismiss();
|
package/tui/src/context/ei.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Match,
|
|
7
7
|
Switch,
|
|
8
8
|
createSignal,
|
|
9
|
+
createMemo,
|
|
9
10
|
type ParentComponent,
|
|
10
11
|
} from "solid-js";
|
|
11
12
|
import { createStore } from "solid-js/store";
|
|
@@ -34,6 +35,7 @@ import type {
|
|
|
34
35
|
LLMRequest,
|
|
35
36
|
} from "../../../src/core/types.js";
|
|
36
37
|
import type { ToolProvider, ToolDefinition } from "../../../src/core/types.js";
|
|
38
|
+
import type { RoomSummary, RoomEntity, RoomMessage, RoomCreationInput } from "../../../src/core/types.js";
|
|
37
39
|
|
|
38
40
|
interface EiStore {
|
|
39
41
|
ready: boolean;
|
|
@@ -43,6 +45,11 @@ interface EiStore {
|
|
|
43
45
|
messages: Message[];
|
|
44
46
|
queueStatus: QueueStatus;
|
|
45
47
|
notification: { message: string; level: "error" | "warn" | "info" } | null;
|
|
48
|
+
rooms: RoomSummary[];
|
|
49
|
+
activeRoomId: string | null;
|
|
50
|
+
roomMessages: RoomMessage[];
|
|
51
|
+
roomActivePath: RoomMessage[];
|
|
52
|
+
isRoomProcessing: boolean;
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
export interface EiContextValue {
|
|
@@ -108,6 +115,30 @@ export interface EiContextValue {
|
|
|
108
115
|
updateTool: (id: string, updates: Partial<Omit<ToolDefinition, 'id' | 'created_at'>>) => Promise<boolean>;
|
|
109
116
|
queueUserDedup: (itemType: "topic" | "person", entityIds: string[]) => void;
|
|
110
117
|
cleanupTimers: () => void;
|
|
118
|
+
rooms: () => RoomSummary[];
|
|
119
|
+
activeRoomId: () => string | null;
|
|
120
|
+
roomMessages: () => RoomMessage[];
|
|
121
|
+
roomActivePath: () => RoomMessage[];
|
|
122
|
+
isRoomProcessing: () => boolean;
|
|
123
|
+
selectRoom: (roomId: string) => void;
|
|
124
|
+
resolveRoomName: (nameOrAlias: string) => string | null;
|
|
125
|
+
getRoom: (roomId: string) => RoomEntity | null;
|
|
126
|
+
createRoom: (input: RoomCreationInput) => Promise<string>;
|
|
127
|
+
updateRoom: (roomId: string, updates: Partial<RoomEntity>) => Promise<void>;
|
|
128
|
+
archiveRoom: (roomId: string) => Promise<void>;
|
|
129
|
+
deleteRoom: (roomId: string) => Promise<void>;
|
|
130
|
+
sendFfaMessage: (content: string | null, silenceReason?: string) => Promise<void>;
|
|
131
|
+
submitHumanRoomMessage: (content: string | null, silenceReason?: string) => string | null;
|
|
132
|
+
recallHumanRoomMessage: () => boolean;
|
|
133
|
+
activateRoom: () => Promise<void>;
|
|
134
|
+
selectCYPBranch: (messageId: string) => Promise<void>;
|
|
135
|
+
markAllRoomMessagesRead: () => Promise<number>;
|
|
136
|
+
captureRoom: () => void;
|
|
137
|
+
capturePersona: () => void;
|
|
138
|
+
sendSilenceMessage: (silenceReason?: string) => Promise<void>;
|
|
139
|
+
humanRoomMessagePending: () => boolean;
|
|
140
|
+
getArchivedRooms: () => RoomSummary[];
|
|
141
|
+
generatePersonaPreview: (name: string, description: string, relationship?: string, personaId?: string) => Promise<import('../../../src/prompts/generation/types.js').PersonaGenerationResult>;
|
|
111
142
|
}
|
|
112
143
|
const EiContext = createContext<EiContextValue>();
|
|
113
144
|
|
|
@@ -120,6 +151,11 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
120
151
|
messages: [],
|
|
121
152
|
queueStatus: { state: "idle", pending_count: 0, dlq_count: 0 },
|
|
122
153
|
notification: null,
|
|
154
|
+
rooms: [],
|
|
155
|
+
activeRoomId: null,
|
|
156
|
+
roomMessages: [],
|
|
157
|
+
roomActivePath: [],
|
|
158
|
+
isRoomProcessing: false,
|
|
123
159
|
});
|
|
124
160
|
|
|
125
161
|
const [contextBoundarySignal, setContextBoundarySignal] = createSignal<string | undefined>(undefined);
|
|
@@ -173,6 +209,7 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
173
209
|
};
|
|
174
210
|
|
|
175
211
|
const selectPersona = (personaId: string) => {
|
|
212
|
+
setStore("activeRoomId", null);
|
|
176
213
|
// Mark previous persona as read ONLY if we dwelled there 5+ seconds
|
|
177
214
|
const previousId = store.activePersonaId;
|
|
178
215
|
if (previousId && previousId === dwelledPersona && processor) {
|
|
@@ -266,6 +303,11 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
266
303
|
return await processor.createPersona(input);
|
|
267
304
|
};
|
|
268
305
|
|
|
306
|
+
const generatePersonaPreview = async (name: string, description: string, relationship?: string, personaId?: string) => {
|
|
307
|
+
if (!processor) throw new Error("Processor not ready");
|
|
308
|
+
return processor.generatePersonaPreview(name, description, relationship, personaId);
|
|
309
|
+
};
|
|
310
|
+
|
|
269
311
|
const archivePersona = async (personaId: string) => {
|
|
270
312
|
if (!processor) return;
|
|
271
313
|
await processor.archivePersona(personaId);
|
|
@@ -456,7 +498,168 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
456
498
|
return processor.searchHumanData(query, options);
|
|
457
499
|
};
|
|
458
500
|
|
|
459
|
-
|
|
501
|
+
const refreshRooms = async () => {
|
|
502
|
+
if (!processor) return;
|
|
503
|
+
const list = processor.getRoomList();
|
|
504
|
+
setStore("rooms", list);
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const refreshRoomMessages = async () => {
|
|
508
|
+
if (!processor) return;
|
|
509
|
+
const currentRoomId = store.activeRoomId;
|
|
510
|
+
if (!currentRoomId) return;
|
|
511
|
+
const msgs = processor.getRoomMessages(currentRoomId);
|
|
512
|
+
setStore("roomMessages", [...msgs]);
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const refreshRoomActivePath = () => {
|
|
516
|
+
if (!processor) return;
|
|
517
|
+
const roomId = store.activeRoomId;
|
|
518
|
+
if (!roomId) return;
|
|
519
|
+
const path = processor.getRoomActivePath(roomId);
|
|
520
|
+
setStore("roomActivePath", [...path]);
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const selectRoom = (roomId: string) => {
|
|
524
|
+
setStore("activeRoomId", roomId);
|
|
525
|
+
setStore("roomMessages", []);
|
|
526
|
+
setStore("roomActivePath", []);
|
|
527
|
+
setStore("isRoomProcessing", false);
|
|
528
|
+
if (processor) {
|
|
529
|
+
const msgs = processor.getRoomMessages(roomId);
|
|
530
|
+
setStore("roomMessages", [...msgs]);
|
|
531
|
+
refreshRoomActivePath();
|
|
532
|
+
if (readTimer) clearTimeout(readTimer);
|
|
533
|
+
readTimer = setTimeout(async () => {
|
|
534
|
+
if (store.activeRoomId === roomId && processor) {
|
|
535
|
+
await processor.markAllRoomMessagesRead(roomId);
|
|
536
|
+
await refreshRooms();
|
|
537
|
+
}
|
|
538
|
+
readTimer = null;
|
|
539
|
+
}, 5000);
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
const sendFfaMessage = async (content: string | null, silenceReason?: string) => {
|
|
544
|
+
const roomId = store.activeRoomId;
|
|
545
|
+
if (!roomId || !processor) return;
|
|
546
|
+
await processor.sendFfaMessage(roomId, content, silenceReason);
|
|
547
|
+
await refreshRooms();
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
const submitHumanRoomMessage = (content: string | null, silenceReason?: string): string | null => {
|
|
551
|
+
const roomId = store.activeRoomId;
|
|
552
|
+
if (!roomId || !processor) return null;
|
|
553
|
+
const msgId = processor.submitHumanRoomMessage(roomId, content, silenceReason);
|
|
554
|
+
void refreshRoomMessages();
|
|
555
|
+
return msgId;
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const recallHumanRoomMessage = (): boolean => {
|
|
559
|
+
const roomId = store.activeRoomId;
|
|
560
|
+
if (!roomId || !processor) return false;
|
|
561
|
+
const recalled = processor.recallHumanRoomMessage(roomId);
|
|
562
|
+
void refreshRoomMessages();
|
|
563
|
+
return recalled;
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const activateRoom = async () => {
|
|
567
|
+
const roomId = store.activeRoomId;
|
|
568
|
+
if (!roomId || !processor) return;
|
|
569
|
+
await processor.activateRoom(roomId);
|
|
570
|
+
await refreshRoomMessages();
|
|
571
|
+
void refreshRoomActivePath();
|
|
572
|
+
void refreshRooms();
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
const selectCYPBranch = async (messageId: string) => {
|
|
576
|
+
const roomId = store.activeRoomId;
|
|
577
|
+
if (!roomId || !processor) return;
|
|
578
|
+
await processor.selectCYPBranch(roomId, messageId);
|
|
579
|
+
await refreshRoomMessages();
|
|
580
|
+
await refreshRoomActivePath();
|
|
581
|
+
void refreshRooms();
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const createRoom = async (input: RoomCreationInput): Promise<string> => {
|
|
585
|
+
if (!processor) return "";
|
|
586
|
+
const roomId = await processor.createRoom(input);
|
|
587
|
+
await refreshRooms();
|
|
588
|
+
selectRoom(roomId);
|
|
589
|
+
return roomId;
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const updateRoom = async (roomId: string, updates: Partial<RoomEntity>) => {
|
|
593
|
+
if (!processor) return;
|
|
594
|
+
await processor.updateRoom(roomId, updates);
|
|
595
|
+
await refreshRooms();
|
|
596
|
+
if (roomId === store.activeRoomId) await refreshRoomMessages();
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const archiveRoom = async (roomId: string) => {
|
|
600
|
+
if (!processor) return;
|
|
601
|
+
await processor.archiveRoom(roomId);
|
|
602
|
+
if (store.activeRoomId === roomId) setStore("activeRoomId", null);
|
|
603
|
+
await refreshRooms();
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
const deleteRoom = async (roomId: string) => {
|
|
607
|
+
if (!processor) return;
|
|
608
|
+
await processor.deleteRoom(roomId);
|
|
609
|
+
if (store.activeRoomId === roomId) setStore("activeRoomId", null);
|
|
610
|
+
await refreshRooms();
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const markAllRoomMessagesRead = async (): Promise<number> => {
|
|
614
|
+
const roomId = store.activeRoomId;
|
|
615
|
+
if (!roomId || !processor) return 0;
|
|
616
|
+
const count = await processor.markAllRoomMessagesRead(roomId);
|
|
617
|
+
await refreshRooms();
|
|
618
|
+
return count;
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
const captureRoom = () => {
|
|
622
|
+
const roomId = store.activeRoomId;
|
|
623
|
+
if (!roomId || !processor) return;
|
|
624
|
+
processor.captureRoom(roomId);
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
const capturePersona = () => {
|
|
628
|
+
const personaId = store.activePersonaId;
|
|
629
|
+
if (!personaId || !processor) return;
|
|
630
|
+
processor.capturePersona(personaId);
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
const resolveRoomName = (nameOrAlias: string): string | null => {
|
|
634
|
+
if (!processor) return null;
|
|
635
|
+
return processor.resolveRoomName(nameOrAlias);
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
const getArchivedRooms = (): RoomSummary[] => {
|
|
639
|
+
if (!processor) return [];
|
|
640
|
+
return processor.getRoomList(true).filter(r => r.is_archived);
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const getRoom = (roomId: string): RoomEntity | null => {
|
|
644
|
+
if (!processor) return null;
|
|
645
|
+
return processor.getRoom(roomId);
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
const humanRoomMessagePending = createMemo(() => {
|
|
649
|
+
const roomId = store.activeRoomId;
|
|
650
|
+
if (!roomId) return false;
|
|
651
|
+
const roomSummary = store.rooms.find(r => r.id === roomId);
|
|
652
|
+
if (!roomSummary?.active_node_id) return false;
|
|
653
|
+
return store.roomMessages.some(m => m.parent_id === roomSummary.active_node_id && m.role === "human");
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
const sendSilenceMessage = async (silenceReason?: string) => {
|
|
657
|
+
const currentId = store.activePersonaId;
|
|
658
|
+
if (!currentId || !processor) return;
|
|
659
|
+
await processor.sendMessage(currentId, null, silenceReason);
|
|
660
|
+
await refreshPersonas();
|
|
661
|
+
};
|
|
662
|
+
|
|
460
663
|
async function finishBootstrap() {
|
|
461
664
|
if (!processor) return;
|
|
462
665
|
|
|
@@ -474,6 +677,7 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
474
677
|
}
|
|
475
678
|
}
|
|
476
679
|
await refreshPersonas();
|
|
680
|
+
await refreshRooms();
|
|
477
681
|
logger.debug(`refreshPersonas done, count: ${store.personas.length}`);
|
|
478
682
|
const status = await processor.getQueueStatus();
|
|
479
683
|
logger.debug("Initial getQueueStatus:", status);
|
|
@@ -589,6 +793,29 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
589
793
|
logger.info("State conflict detected, waiting for user resolution");
|
|
590
794
|
setConflictData(data);
|
|
591
795
|
},
|
|
796
|
+
onRoomAdded: () => void refreshRooms(),
|
|
797
|
+
onRoomRemoved: () => void refreshRooms(),
|
|
798
|
+
onRoomUpdated: (roomId) => {
|
|
799
|
+
void refreshRooms();
|
|
800
|
+
if (roomId === store.activeRoomId) {
|
|
801
|
+
void refreshRoomMessages();
|
|
802
|
+
refreshRoomActivePath();
|
|
803
|
+
}
|
|
804
|
+
},
|
|
805
|
+
onRoomMessageAdded: (roomId) => {
|
|
806
|
+
void refreshRooms();
|
|
807
|
+
if (roomId === store.activeRoomId) {
|
|
808
|
+
void refreshRoomMessages();
|
|
809
|
+
refreshRoomActivePath();
|
|
810
|
+
setStore("isRoomProcessing", false);
|
|
811
|
+
}
|
|
812
|
+
},
|
|
813
|
+
onRoomMessageQueued: () => {
|
|
814
|
+
void refreshRooms();
|
|
815
|
+
},
|
|
816
|
+
onRoomMessageProcessing: (roomId) => {
|
|
817
|
+
if (roomId === store.activeRoomId) setStore("isRoomProcessing", true);
|
|
818
|
+
},
|
|
592
819
|
};
|
|
593
820
|
processor = new Processor(eiInterface);
|
|
594
821
|
logger.debug("Processor created, calling start()");
|
|
@@ -671,6 +898,30 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
671
898
|
updateTool,
|
|
672
899
|
queueUserDedup,
|
|
673
900
|
cleanupTimers,
|
|
901
|
+
rooms: () => store.rooms,
|
|
902
|
+
activeRoomId: () => store.activeRoomId,
|
|
903
|
+
roomMessages: () => store.roomMessages,
|
|
904
|
+
roomActivePath: () => store.roomActivePath,
|
|
905
|
+
isRoomProcessing: () => store.isRoomProcessing,
|
|
906
|
+
selectRoom,
|
|
907
|
+
resolveRoomName,
|
|
908
|
+
getRoom,
|
|
909
|
+
createRoom,
|
|
910
|
+
updateRoom,
|
|
911
|
+
archiveRoom,
|
|
912
|
+
deleteRoom,
|
|
913
|
+
sendFfaMessage,
|
|
914
|
+
submitHumanRoomMessage,
|
|
915
|
+
recallHumanRoomMessage,
|
|
916
|
+
activateRoom,
|
|
917
|
+
selectCYPBranch,
|
|
918
|
+
markAllRoomMessagesRead,
|
|
919
|
+
captureRoom,
|
|
920
|
+
capturePersona,
|
|
921
|
+
sendSilenceMessage,
|
|
922
|
+
humanRoomMessagePending,
|
|
923
|
+
getArchivedRooms,
|
|
924
|
+
generatePersonaPreview,
|
|
674
925
|
};
|
|
675
926
|
return (
|
|
676
927
|
<Switch>
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "solid-js";
|
|
8
8
|
import { useKeyboard, useRenderer, useSelectionHandler } from "@opentui/solid";
|
|
9
9
|
import type { ScrollBoxRenderable, KeyEvent, TextareaRenderable, CliRenderer } from "@opentui/core";
|
|
10
|
-
import type { PersonaSummary } from "../../../src/core/types.js";
|
|
10
|
+
import type { PersonaSummary, RoomSummary } from "../../../src/core/types.js";
|
|
11
11
|
import { useEi } from "./ei";
|
|
12
12
|
import { logger } from "../util/logger";
|
|
13
13
|
import { copyToClipboard } from "../util/clipboard";
|
|
@@ -25,6 +25,8 @@ interface KeyboardContextValue {
|
|
|
25
25
|
exitApp: () => Promise<void>;
|
|
26
26
|
renderer: CliRenderer;
|
|
27
27
|
resetHistoryIndex: () => void;
|
|
28
|
+
overlayActive: () => boolean;
|
|
29
|
+
setOverlayActive: (active: boolean) => void;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
const KeyboardContext = createContext<KeyboardContextValue>();
|
|
@@ -32,8 +34,9 @@ const KeyboardContext = createContext<KeyboardContextValue>();
|
|
|
32
34
|
export const KeyboardProvider: ParentComponent = (props) => {
|
|
33
35
|
const [focusedPanel, setFocusedPanel] = createSignal<Panel>("input");
|
|
34
36
|
const [sidebarVisible, setSidebarVisible] = createSignal(true);
|
|
37
|
+
const [overlayActive, setOverlayActive] = createSignal(false);
|
|
35
38
|
const renderer = useRenderer();
|
|
36
|
-
const { queueStatus, abortCurrentOperation, resumeQueue, pauseQueue, personas, activePersonaId, selectPersona, saveAndExit, showNotification, messages, recallPendingMessages, cleanupTimers } = useEi();
|
|
39
|
+
const { queueStatus, abortCurrentOperation, resumeQueue, pauseQueue, personas, activePersonaId, selectPersona, saveAndExit, showNotification, messages, recallPendingMessages, cleanupTimers, rooms, activeRoomId, selectRoom } = useEi();
|
|
37
40
|
|
|
38
41
|
let messageScrollRef: ScrollBoxRenderable | null = null;
|
|
39
42
|
let textareaRef: TextareaRenderable | null = null;
|
|
@@ -76,13 +79,26 @@ export const KeyboardProvider: ParentComponent = (props) => {
|
|
|
76
79
|
if (event.name === "tab") {
|
|
77
80
|
event.preventDefault();
|
|
78
81
|
if (textareaRef && textareaRef.plainText.length > 0) return;
|
|
79
|
-
|
|
82
|
+
|
|
83
|
+
if (activeRoomId()) {
|
|
84
|
+
const activeRooms = rooms().filter((r: RoomSummary) => !r.is_archived);
|
|
85
|
+
if (activeRooms.length <= 1) return;
|
|
86
|
+
const current = activeRoomId();
|
|
87
|
+
const currentIndex = activeRooms.findIndex((r: RoomSummary) => r.id === current);
|
|
88
|
+
let nextIndex: number;
|
|
89
|
+
if (event.shift) {
|
|
90
|
+
nextIndex = (currentIndex - 1 + activeRooms.length) % activeRooms.length;
|
|
91
|
+
} else {
|
|
92
|
+
nextIndex = (currentIndex + 1) % activeRooms.length;
|
|
93
|
+
}
|
|
94
|
+
selectRoom(activeRooms[nextIndex].id);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
80
98
|
const unarchived = personas().filter((p: PersonaSummary) => !p.is_archived);
|
|
81
99
|
if (unarchived.length <= 1) return;
|
|
82
|
-
|
|
83
100
|
const current = activePersonaId();
|
|
84
101
|
const currentIndex = unarchived.findIndex((p: PersonaSummary) => p.id === current);
|
|
85
|
-
|
|
86
102
|
let nextIndex: number;
|
|
87
103
|
if (event.shift) {
|
|
88
104
|
nextIndex = (currentIndex - 1 + unarchived.length) % unarchived.length;
|
|
@@ -120,6 +136,7 @@ export const KeyboardProvider: ParentComponent = (props) => {
|
|
|
120
136
|
}
|
|
121
137
|
|
|
122
138
|
if (event.name === "escape") {
|
|
139
|
+
if (overlayActive()) return;
|
|
123
140
|
event.preventDefault();
|
|
124
141
|
const status = queueStatus();
|
|
125
142
|
|
|
@@ -137,7 +154,7 @@ export const KeyboardProvider: ParentComponent = (props) => {
|
|
|
137
154
|
}
|
|
138
155
|
|
|
139
156
|
|
|
140
|
-
if (event.name === "up" && !event.ctrl && !event.shift && !event.meta) {
|
|
157
|
+
if (event.name === "up" && !overlayActive() && !activeRoomId() && !event.ctrl && !event.shift && !event.meta) {
|
|
141
158
|
if (!textareaRef) return;
|
|
142
159
|
const cursor = textareaRef.logicalCursor;
|
|
143
160
|
// Only intercept when cursor is at the very beginning (row 0, col 0)
|
|
@@ -174,7 +191,7 @@ export const KeyboardProvider: ParentComponent = (props) => {
|
|
|
174
191
|
return;
|
|
175
192
|
}
|
|
176
193
|
|
|
177
|
-
if (event.name === "down" && !event.ctrl && !event.shift && !event.meta) {
|
|
194
|
+
if (event.name === "down" && !overlayActive() && !activeRoomId() && !event.ctrl && !event.shift && !event.meta) {
|
|
178
195
|
if (!textareaRef || historyIndex === -1) return;
|
|
179
196
|
// Only intercept when cursor is at the very end
|
|
180
197
|
if (textareaRef.cursorOffset !== textareaRef.plainText.length) return;
|
|
@@ -199,7 +216,7 @@ export const KeyboardProvider: ParentComponent = (props) => {
|
|
|
199
216
|
|
|
200
217
|
if (!messageScrollRef) return;
|
|
201
218
|
|
|
202
|
-
const scrollAmount = messageScrollRef.height;
|
|
219
|
+
const scrollAmount = Math.floor(messageScrollRef.height / 2);
|
|
203
220
|
|
|
204
221
|
if (event.name === "pageup") {
|
|
205
222
|
event.preventDefault();
|
|
@@ -243,6 +260,8 @@ export const KeyboardProvider: ParentComponent = (props) => {
|
|
|
243
260
|
exitApp,
|
|
244
261
|
renderer,
|
|
245
262
|
resetHistoryIndex,
|
|
263
|
+
overlayActive,
|
|
264
|
+
setOverlayActive,
|
|
246
265
|
};
|
|
247
266
|
|
|
248
267
|
return (
|
|
@@ -252,10 +271,27 @@ export const KeyboardProvider: ParentComponent = (props) => {
|
|
|
252
271
|
);
|
|
253
272
|
};
|
|
254
273
|
|
|
274
|
+
// No-op stub returned when useKeyboardNav is called outside KeyboardProvider.
|
|
275
|
+
// ConflictOverlay is the only legitimate caller in this situation — it renders
|
|
276
|
+
// before KeyboardProvider exists because the app can't boot (state conflict).
|
|
277
|
+
// All keyboard operations are genuinely meaningless at that point; the stub
|
|
278
|
+
// makes setOverlayActive calls harmless rather than fatal.
|
|
279
|
+
const KEYBOARD_NOOP: KeyboardContextValue = {
|
|
280
|
+
focusedPanel: () => "input" as Panel,
|
|
281
|
+
setFocusedPanel: () => {},
|
|
282
|
+
registerMessageScroll: () => {},
|
|
283
|
+
registerTextarea: () => {},
|
|
284
|
+
registerEditorHandler: () => {},
|
|
285
|
+
sidebarVisible: () => true,
|
|
286
|
+
toggleSidebar: () => {},
|
|
287
|
+
exitApp: async () => {},
|
|
288
|
+
renderer: null as unknown as CliRenderer,
|
|
289
|
+
resetHistoryIndex: () => {},
|
|
290
|
+
overlayActive: () => false,
|
|
291
|
+
setOverlayActive: () => {},
|
|
292
|
+
};
|
|
293
|
+
|
|
255
294
|
export const useKeyboardNav = () => {
|
|
256
295
|
const ctx = useContext(KeyboardContext);
|
|
257
|
-
|
|
258
|
-
throw new Error("useKeyboardNav must be used within KeyboardProvider");
|
|
259
|
-
}
|
|
260
|
-
return ctx;
|
|
296
|
+
return ctx ?? KEYBOARD_NOOP;
|
|
261
297
|
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { spawnEditor } from "./editor.js";
|
|
2
|
+
import type { CliRenderer } from "@opentui/core";
|
|
3
|
+
import type { RoomMessage, PersonaSummary } from "../../../src/core/types.js";
|
|
4
|
+
|
|
5
|
+
interface ParsedBlock {
|
|
6
|
+
id: string;
|
|
7
|
+
chosen: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function buildCYPEditorYAML(
|
|
11
|
+
activeNodeId: string,
|
|
12
|
+
messages: RoomMessage[],
|
|
13
|
+
personas: PersonaSummary[]
|
|
14
|
+
): string {
|
|
15
|
+
const children = messages.filter((m) => m.parent_id === activeNodeId);
|
|
16
|
+
|
|
17
|
+
const header = `# Choose Your Path
|
|
18
|
+
# Mark exactly ONE response with _chosen: true to continue the story.
|
|
19
|
+
# Save and exit to confirm your selection.
|
|
20
|
+
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
const isExplored = (msgId: string) => messages.some((x) => x.parent_id === msgId);
|
|
24
|
+
|
|
25
|
+
const blocks = children.map((m) => {
|
|
26
|
+
let speaker = "You";
|
|
27
|
+
if (m.role === "persona" && m.persona_id) {
|
|
28
|
+
speaker = personas.find((p) => p.id === m.persona_id)?.display_name ?? m.persona_id;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const contentLines: string[] = [];
|
|
32
|
+
if (m.verbal_response !== undefined) {
|
|
33
|
+
const indented = m.verbal_response.split("\n").map((l) => ` ${l}`).join("\n");
|
|
34
|
+
contentLines.push(` verbal_response: |\n${indented}`);
|
|
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}`);
|
|
39
|
+
}
|
|
40
|
+
if (m.silence_reason !== undefined && m.verbal_response === undefined) {
|
|
41
|
+
contentLines.push(` silence_reason: "${m.silence_reason}"`);
|
|
42
|
+
}
|
|
43
|
+
if (contentLines.length === 0) {
|
|
44
|
+
contentLines.push(` # (no content)`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return `- id: "${m.id}"
|
|
48
|
+
speaker: "${speaker}"
|
|
49
|
+
explored: ${isExplored(m.id)}
|
|
50
|
+
_chosen: false
|
|
51
|
+
${contentLines.join("\n")}`;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return header + blocks.join("\n\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function parseCYPEditorYAML(content: string): ParsedBlock[] {
|
|
58
|
+
const blocks: ParsedBlock[] = [];
|
|
59
|
+
let currentId: string | null = null;
|
|
60
|
+
let currentChosen = false;
|
|
61
|
+
let inContent = false;
|
|
62
|
+
|
|
63
|
+
for (const line of content.split("\n")) {
|
|
64
|
+
const trimmed = line.trim();
|
|
65
|
+
|
|
66
|
+
if (trimmed.startsWith("#") || trimmed === "") continue;
|
|
67
|
+
|
|
68
|
+
// New block starts with `- id:` at column 0
|
|
69
|
+
if (line.startsWith("- id:")) {
|
|
70
|
+
if (currentId !== null) {
|
|
71
|
+
blocks.push({ id: currentId, chosen: currentChosen });
|
|
72
|
+
}
|
|
73
|
+
const val = trimmed.slice("- id:".length).trim().replace(/^["']|["']$/g, "");
|
|
74
|
+
currentId = val;
|
|
75
|
+
currentChosen = false;
|
|
76
|
+
inContent = false;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Content block lines are indented with 4 spaces — skip them
|
|
81
|
+
if (inContent && line.startsWith(" ")) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
inContent = false;
|
|
85
|
+
|
|
86
|
+
if (currentId !== null) {
|
|
87
|
+
if (trimmed.startsWith("_chosen:")) {
|
|
88
|
+
const val = trimmed.slice("_chosen:".length).trim().toLowerCase();
|
|
89
|
+
currentChosen = val === "true";
|
|
90
|
+
} else if (trimmed.startsWith("content:")) {
|
|
91
|
+
inContent = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (currentId !== null) {
|
|
97
|
+
blocks.push({ id: currentId, chosen: currentChosen });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return blocks;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function openCYPEditor(opts: {
|
|
104
|
+
roomId: string;
|
|
105
|
+
activeNodeId: string;
|
|
106
|
+
messages: RoomMessage[];
|
|
107
|
+
activePath: RoomMessage[];
|
|
108
|
+
personas: PersonaSummary[];
|
|
109
|
+
selectBranch: (messageId: string) => Promise<void>;
|
|
110
|
+
showNotification: (msg: string, level: "error" | "warn" | "info") => void;
|
|
111
|
+
renderer: CliRenderer;
|
|
112
|
+
}): Promise<void> {
|
|
113
|
+
const { roomId, activeNodeId, messages, personas, selectBranch, showNotification, renderer } = opts;
|
|
114
|
+
|
|
115
|
+
const yamlContent = buildCYPEditorYAML(activeNodeId, messages, personas);
|
|
116
|
+
|
|
117
|
+
const result = await spawnEditor({
|
|
118
|
+
initialContent: yamlContent,
|
|
119
|
+
filename: `${roomId}-choose.yaml`,
|
|
120
|
+
renderer,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (result.aborted) {
|
|
124
|
+
showNotification("Editor cancelled", "info");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!result.success) {
|
|
129
|
+
showNotification("Editor failed to open", "error");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (result.content === null) {
|
|
134
|
+
showNotification("No changes made", "info");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const blocks = parseCYPEditorYAML(result.content);
|
|
139
|
+
const chosenBlocks = blocks.filter((b) => b.chosen);
|
|
140
|
+
|
|
141
|
+
if (chosenBlocks.length === 0) {
|
|
142
|
+
showNotification("No response chosen — editor cancelled", "warn");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (chosenBlocks.length > 1) {
|
|
147
|
+
showNotification("Mark exactly one response — try again", "warn");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await selectBranch(chosenBlocks[0].id);
|
|
152
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Quote } from "../../../src/core/types.js";
|
|
2
|
+
|
|
3
|
+
export function insertQuoteMarkers(content: string, quotes: Quote[]): string {
|
|
4
|
+
const validQuotes = quotes
|
|
5
|
+
.filter(q => q.end !== null && q.end !== undefined)
|
|
6
|
+
.sort((a, b) => b.end! - a.end!);
|
|
7
|
+
|
|
8
|
+
let result = content;
|
|
9
|
+
for (const quote of validQuotes) {
|
|
10
|
+
let insertPos = quote.end!;
|
|
11
|
+
if (insertPos >= 0 && insertPos <= result.length) {
|
|
12
|
+
while (insertPos > 0 && (result[insertPos - 1] === '\n' || result[insertPos - 1] === ' ')) {
|
|
13
|
+
insertPos--;
|
|
14
|
+
}
|
|
15
|
+
result = result.slice(0, insertPos) + "\u207a" + result.slice(insertPos);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|