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,32 +1,37 @@
|
|
|
1
1
|
import type { Command } from "./registry";
|
|
2
|
-
import {
|
|
2
|
+
import { ArchivedItemsOverlay } from "../components/ArchivedItemsOverlay";
|
|
3
3
|
|
|
4
4
|
export const archiveCommand: Command = {
|
|
5
5
|
name: "archive",
|
|
6
6
|
aliases: [],
|
|
7
|
-
description: "Archive a persona or list archived personas",
|
|
7
|
+
description: "Archive a persona or list archived personas and rooms",
|
|
8
8
|
usage: "/archive [name]",
|
|
9
9
|
|
|
10
10
|
async execute(args, ctx) {
|
|
11
11
|
const allPersonas = ctx.ei.personas();
|
|
12
|
-
const
|
|
12
|
+
const archivedPersonas = allPersonas.filter(p => p.is_archived);
|
|
13
13
|
|
|
14
14
|
if (args.length === 0) {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
const archivedRooms = ctx.ei.getArchivedRooms();
|
|
16
|
+
if (archivedPersonas.length === 0 && archivedRooms.length === 0) {
|
|
17
|
+
ctx.showNotification("No archived personas or rooms", "info");
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
19
20
|
ctx.showOverlay((hideOverlay, _hideForEditor) => (
|
|
20
|
-
<
|
|
21
|
-
personas={
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
onSelect={async (personaId) => {
|
|
25
|
-
const persona = archived.find(p => p.id === personaId);
|
|
21
|
+
<ArchivedItemsOverlay
|
|
22
|
+
personas={archivedPersonas}
|
|
23
|
+
rooms={archivedRooms}
|
|
24
|
+
onSelect={async (item) => {
|
|
26
25
|
hideOverlay();
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
if (item.kind === "persona") {
|
|
27
|
+
await ctx.ei.unarchivePersona(item.id);
|
|
28
|
+
ctx.ei.selectPersona(item.id);
|
|
29
|
+
ctx.showNotification(`Unarchived and switched to ${item.display_name}`, "info");
|
|
30
|
+
} else {
|
|
31
|
+
await ctx.ei.updateRoom(item.id, { is_archived: false });
|
|
32
|
+
ctx.ei.selectRoom(item.id);
|
|
33
|
+
ctx.showNotification(`Room "${item.display_name}" unarchived`, "info");
|
|
34
|
+
}
|
|
30
35
|
}}
|
|
31
36
|
onDismiss={hideOverlay}
|
|
32
37
|
/>
|
|
@@ -38,7 +43,18 @@ export const archiveCommand: Command = {
|
|
|
38
43
|
const personaId = await ctx.ei.resolvePersonaName(nameOrAlias);
|
|
39
44
|
|
|
40
45
|
if (!personaId) {
|
|
41
|
-
ctx.
|
|
46
|
+
const roomId = ctx.ei.resolveRoomName(nameOrAlias);
|
|
47
|
+
if (roomId) {
|
|
48
|
+
const room = ctx.ei.getRoom(roomId);
|
|
49
|
+
if (room?.is_archived) {
|
|
50
|
+
ctx.showNotification(`Room "${room.display_name}" is already archived`, "warn");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
await ctx.ei.archiveRoom(roomId);
|
|
54
|
+
ctx.showNotification(`Room "${room?.display_name ?? nameOrAlias}" archived`, "info");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
ctx.showNotification(`Persona or room '${nameOrAlias}' not found`, "error");
|
|
42
58
|
return;
|
|
43
59
|
}
|
|
44
60
|
|
|
@@ -62,7 +78,7 @@ export const archiveCommand: Command = {
|
|
|
62
78
|
export const unarchiveCommand: Command = {
|
|
63
79
|
name: "unarchive",
|
|
64
80
|
aliases: [],
|
|
65
|
-
description: "Unarchive a persona and switch to it",
|
|
81
|
+
description: "Unarchive a persona or room and switch to it",
|
|
66
82
|
usage: "/unarchive <name>",
|
|
67
83
|
|
|
68
84
|
async execute(args, ctx) {
|
|
@@ -74,20 +90,33 @@ export const unarchiveCommand: Command = {
|
|
|
74
90
|
const nameOrAlias = args.join(" ");
|
|
75
91
|
const personaId = await ctx.ei.resolvePersonaName(nameOrAlias);
|
|
76
92
|
|
|
77
|
-
if (
|
|
78
|
-
ctx.
|
|
93
|
+
if (personaId) {
|
|
94
|
+
const persona = ctx.ei.personas().find(p => p.id === personaId);
|
|
95
|
+
|
|
96
|
+
if (!persona?.is_archived) {
|
|
97
|
+
ctx.showNotification(`'${persona?.display_name ?? nameOrAlias}' is not archived`, "warn");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await ctx.ei.unarchivePersona(personaId);
|
|
102
|
+
ctx.ei.selectPersona(personaId);
|
|
103
|
+
ctx.showNotification(`Unarchived and switched to ${persona.display_name}`, "info");
|
|
79
104
|
return;
|
|
80
105
|
}
|
|
81
106
|
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
107
|
+
const roomId = ctx.ei.resolveRoomName(nameOrAlias);
|
|
108
|
+
if (roomId) {
|
|
109
|
+
const room = ctx.ei.getRoom(roomId);
|
|
110
|
+
if (!room?.is_archived) {
|
|
111
|
+
ctx.showNotification(`Room '${room?.display_name ?? nameOrAlias}' is not archived`, "warn");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
await ctx.ei.updateRoom(roomId, { is_archived: false });
|
|
115
|
+
ctx.ei.selectRoom(roomId);
|
|
116
|
+
ctx.showNotification(`Room "${room?.display_name ?? nameOrAlias}" unarchived`, "info");
|
|
86
117
|
return;
|
|
87
118
|
}
|
|
88
119
|
|
|
89
|
-
|
|
90
|
-
ctx.ei.selectPersona(personaId);
|
|
91
|
-
ctx.showNotification(`Unarchived and switched to ${persona.display_name}`, "info");
|
|
120
|
+
ctx.showNotification(`Archived persona or room '${nameOrAlias}' not found`, "error");
|
|
92
121
|
}
|
|
93
122
|
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Command } from "./registry";
|
|
2
|
+
|
|
3
|
+
export const captureCommand: Command = {
|
|
4
|
+
name: "capture",
|
|
5
|
+
aliases: [],
|
|
6
|
+
description: "Trigger extraction on current chat",
|
|
7
|
+
usage: "/capture | /capture opencode",
|
|
8
|
+
|
|
9
|
+
async execute(args, ctx) {
|
|
10
|
+
if (args.length === 0) {
|
|
11
|
+
if (ctx.ei.activeRoomId()) {
|
|
12
|
+
ctx.ei.captureRoom();
|
|
13
|
+
ctx.showNotification("Extraction queued for room", "info");
|
|
14
|
+
} else {
|
|
15
|
+
ctx.ei.capturePersona();
|
|
16
|
+
ctx.showNotification("Extraction queued", "info");
|
|
17
|
+
}
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const integrationMap: Record<string, "opencode" | "claudeCode" | "cursor"> = {
|
|
22
|
+
opencode: "opencode",
|
|
23
|
+
claudecode: "claudeCode",
|
|
24
|
+
cursor: "cursor",
|
|
25
|
+
};
|
|
26
|
+
const integrationKey = integrationMap[args[0].toLowerCase()];
|
|
27
|
+
if (integrationKey) {
|
|
28
|
+
const human = await ctx.ei.getHuman();
|
|
29
|
+
const intSettings = human.settings?.[integrationKey];
|
|
30
|
+
if (!intSettings?.integration) {
|
|
31
|
+
ctx.showNotification(`${args[0]} integration not enabled. Enable in /settings.`, "warn");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// Reset last_sync to epoch to force immediate scan on next processor tick
|
|
35
|
+
await ctx.ei.updateHuman({
|
|
36
|
+
settings: {
|
|
37
|
+
...human.settings,
|
|
38
|
+
[integrationKey]: {
|
|
39
|
+
...intSettings,
|
|
40
|
+
last_sync: new Date(0).toISOString(),
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
ctx.showNotification(`${args[0]} scan will start on next cycle`, "info");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
ctx.showNotification("Named capture not yet supported. Use /room or /persona to switch first.", "warn");
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -11,7 +11,7 @@ function buildDedupeYAML(type: DedupeType, terms: string[], entities: Array<Topi
|
|
|
11
11
|
`# /dedupe ${type} ${terms.map(t => t.includes(" ") ? `"${t}"` : t).join(" ")}`,
|
|
12
12
|
`# Terms: ${termDisplay}`,
|
|
13
13
|
`# Found ${entities.length} match${entities.length === 1 ? "" : "es"}. DELETE blocks for entries to EXCLUDE from the merge.`,
|
|
14
|
-
`# Keep at least 2. Save
|
|
14
|
+
`# Keep at least 2. Save or quit to confirm • :cq to cancel.`,
|
|
15
15
|
``,
|
|
16
16
|
].join("\n");
|
|
17
17
|
|
|
@@ -129,12 +129,7 @@ export const dedupeCommand: Command = {
|
|
|
129
129
|
return;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
ctx.showNotification("No changes — dedupe cancelled", "info");
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const keptIds = parseDedupeYAML(result.content);
|
|
132
|
+
const keptIds = parseDedupeYAML(result.content ?? yamlContent);
|
|
138
133
|
|
|
139
134
|
if (keptIds.length < 2) {
|
|
140
135
|
ctx.showNotification("Need at least 2 entries to merge — dedupe cancelled", "error");
|
|
@@ -9,6 +9,30 @@ export const deleteCommand: Command = {
|
|
|
9
9
|
usage: "/delete [name]",
|
|
10
10
|
|
|
11
11
|
async execute(args, ctx) {
|
|
12
|
+
if (ctx.ei.activeRoomId() && args.length === 0) {
|
|
13
|
+
const roomId = ctx.ei.activeRoomId()!;
|
|
14
|
+
const room = ctx.ei.getRoom(roomId);
|
|
15
|
+
const displayName = room?.display_name ?? roomId;
|
|
16
|
+
|
|
17
|
+
const confirmed = await new Promise<boolean>((resolve) => {
|
|
18
|
+
ctx.showOverlay((hideOverlay, _hideForEditor) => (
|
|
19
|
+
<ConfirmOverlay
|
|
20
|
+
message={`Archive room "${displayName}"?\nThe room will be archived and removed from the list.`}
|
|
21
|
+
onConfirm={() => { hideOverlay(); resolve(true); }}
|
|
22
|
+
onCancel={() => { hideOverlay(); resolve(false); }}
|
|
23
|
+
/>
|
|
24
|
+
), ctx.renderer);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (confirmed) {
|
|
28
|
+
await ctx.ei.archiveRoom(roomId);
|
|
29
|
+
ctx.showNotification(`Room "${displayName}" archived`, "info");
|
|
30
|
+
} else {
|
|
31
|
+
ctx.showNotification("Cancelled", "info");
|
|
32
|
+
}
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
12
36
|
const allPersonas = ctx.ei.personas();
|
|
13
37
|
const deletable = allPersonas.filter(p => p.id !== ctx.ei.activePersonaId());
|
|
14
38
|
|
|
@@ -56,6 +80,30 @@ export const deleteCommand: Command = {
|
|
|
56
80
|
const personaId = await ctx.ei.resolvePersonaName(nameOrAlias);
|
|
57
81
|
|
|
58
82
|
if (!personaId) {
|
|
83
|
+
const roomId = ctx.ei.resolveRoomName(nameOrAlias);
|
|
84
|
+
if (roomId) {
|
|
85
|
+
const room = ctx.ei.getRoom(roomId);
|
|
86
|
+
const displayName = room?.display_name ?? roomId;
|
|
87
|
+
|
|
88
|
+
const confirmed = await new Promise<boolean>((resolve) => {
|
|
89
|
+
ctx.showOverlay((hideOverlay, _hideForEditor) => (
|
|
90
|
+
<ConfirmOverlay
|
|
91
|
+
message={`Archive room "${displayName}"?\nThe room will be archived and removed from the list.`}
|
|
92
|
+
onConfirm={() => { hideOverlay(); resolve(true); }}
|
|
93
|
+
onCancel={() => { hideOverlay(); resolve(false); }}
|
|
94
|
+
/>
|
|
95
|
+
), ctx.renderer);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (confirmed) {
|
|
99
|
+
await ctx.ei.archiveRoom(roomId);
|
|
100
|
+
ctx.showNotification(`Room "${displayName}" archived`, "info");
|
|
101
|
+
} else {
|
|
102
|
+
ctx.showNotification("Cancelled", "info");
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
59
107
|
ctx.showNotification(`Persona '${nameOrAlias}' not found`, "error");
|
|
60
108
|
return;
|
|
61
109
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Command } from "./registry.js";
|
|
2
2
|
import { openPersonaEditor } from "../util/persona-editor.js";
|
|
3
|
+
import { openRoomEditor } from "../util/room-editor.js";
|
|
3
4
|
|
|
4
5
|
export const detailsCommand: Command = {
|
|
5
6
|
name: "details",
|
|
@@ -8,6 +9,12 @@ export const detailsCommand: Command = {
|
|
|
8
9
|
usage: "/details [persona] - Edit specified or current persona",
|
|
9
10
|
|
|
10
11
|
async execute(args, ctx) {
|
|
12
|
+
if (args.length === 0 && ctx.ei.activeRoomId()) {
|
|
13
|
+
const roomId = ctx.ei.activeRoomId()!;
|
|
14
|
+
await openRoomEditor({ roomId, ctx });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
let personaId: string | null;
|
|
12
19
|
|
|
13
20
|
if (args.length > 0) {
|
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
import type { Command } from "./registry";
|
|
2
2
|
import { isReservedPersonaName } from "../../../src/core/types.js";
|
|
3
3
|
import { PersonaListOverlay } from "../components/PersonaListOverlay";
|
|
4
|
-
import {
|
|
4
|
+
import { LoadingOverlay } from "../components/LoadingOverlay.js";
|
|
5
|
+
import { PersonPickerOverlay } from "../components/PersonPickerOverlay.js";
|
|
6
|
+
import type { PersonPickerItem } from "../components/PersonPickerOverlay.js";
|
|
7
|
+
import { ConfirmOverlay } from "../components/ConfirmOverlay.js";
|
|
8
|
+
import { spawnEditor } from "../util/editor.js";
|
|
9
|
+
import {
|
|
10
|
+
descriptionEntryToYAML,
|
|
11
|
+
descriptionFromYAML,
|
|
12
|
+
personaPreviewToYAML,
|
|
13
|
+
personaPreviewFromYAML,
|
|
14
|
+
} from "../util/yaml-serializers.js";
|
|
5
15
|
|
|
6
16
|
export const personaCommand: Command = {
|
|
7
17
|
name: "persona",
|
|
8
18
|
aliases: ["p"],
|
|
9
|
-
description: "Switch persona, list all, or
|
|
10
|
-
usage: "/persona [name] | /persona new <name>",
|
|
11
|
-
|
|
19
|
+
description: "Switch persona, list all, create new, or update from person",
|
|
20
|
+
usage: "/persona [name] | /persona new <name> | /persona update <personaName> <personName>",
|
|
21
|
+
|
|
12
22
|
async execute(args, ctx) {
|
|
13
23
|
const unarchived = ctx.ei.personas().filter(p => !p.is_archived);
|
|
14
|
-
|
|
24
|
+
|
|
15
25
|
if (args.length === 0) {
|
|
16
26
|
ctx.showOverlay((hideOverlay, _hideForEditor) => (
|
|
17
27
|
<PersonaListOverlay
|
|
@@ -28,7 +38,7 @@ export const personaCommand: Command = {
|
|
|
28
38
|
), ctx.renderer);
|
|
29
39
|
return;
|
|
30
40
|
}
|
|
31
|
-
|
|
41
|
+
|
|
32
42
|
if (args[0].toLowerCase() === "new") {
|
|
33
43
|
if (args.length < 2) {
|
|
34
44
|
ctx.showNotification("Usage: /p new <name>", "error");
|
|
@@ -39,14 +49,266 @@ export const personaCommand: Command = {
|
|
|
39
49
|
ctx.showNotification(`Cannot use reserved name "${personaName}"`, "error");
|
|
40
50
|
return;
|
|
41
51
|
}
|
|
42
|
-
|
|
52
|
+
|
|
53
|
+
// Step 1: description editor
|
|
54
|
+
const descResult = await spawnEditor({
|
|
55
|
+
initialContent: descriptionEntryToYAML(personaName),
|
|
56
|
+
filename: `${personaName}-description.yaml`,
|
|
57
|
+
renderer: ctx.renderer,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (descResult.aborted || descResult.content === null) {
|
|
61
|
+
ctx.showNotification("Cancelled", "info");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let parsed: { description: string; relationship?: string };
|
|
66
|
+
try {
|
|
67
|
+
parsed = descriptionFromYAML(descResult.content);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
ctx.showNotification(`Parse error: ${e instanceof Error ? e.message : String(e)}`, "error");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!parsed.description) {
|
|
74
|
+
ctx.showNotification("No description provided", "error");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Step 2: generate preview with loading overlay
|
|
79
|
+
let dismissed = false;
|
|
80
|
+
const overlayCallbacks = { hideOverlay: null as (() => void) | null, hideForEditor: null as (() => void) | null };
|
|
81
|
+
|
|
82
|
+
const previewPromise = new Promise<import('../../../src/prompts/generation/types.js').PersonaGenerationResult | null>((resolve) => {
|
|
83
|
+
ctx.showOverlay((hideOverlay, hideForEditor) => {
|
|
84
|
+
overlayCallbacks.hideOverlay = hideOverlay;
|
|
85
|
+
overlayCallbacks.hideForEditor = hideForEditor;
|
|
86
|
+
return (
|
|
87
|
+
<LoadingOverlay
|
|
88
|
+
message={`Generating persona preview for "${personaName}"...`}
|
|
89
|
+
onCancel={() => {
|
|
90
|
+
dismissed = true;
|
|
91
|
+
hideOverlay();
|
|
92
|
+
resolve(null);
|
|
93
|
+
}}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
}, ctx.renderer);
|
|
97
|
+
|
|
98
|
+
ctx.ei.generatePersonaPreview(personaName, parsed.description, parsed.relationship)
|
|
99
|
+
.then(result => {
|
|
100
|
+
if (!dismissed && overlayCallbacks.hideOverlay) {
|
|
101
|
+
resolve(result);
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
.catch(e => {
|
|
105
|
+
if (!dismissed && overlayCallbacks.hideOverlay) {
|
|
106
|
+
overlayCallbacks.hideOverlay();
|
|
107
|
+
ctx.showNotification(`Generation failed: ${e instanceof Error ? e.message : String(e)}`, "error");
|
|
108
|
+
resolve(null);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const preview = await previewPromise;
|
|
114
|
+
if (!preview) return;
|
|
115
|
+
|
|
116
|
+
overlayCallbacks.hideForEditor?.();
|
|
117
|
+
|
|
118
|
+
// Step 3: review editor
|
|
119
|
+
const previewYAML = personaPreviewToYAML(preview, personaName);
|
|
120
|
+
const reviewResult = await spawnEditor({
|
|
121
|
+
initialContent: previewYAML,
|
|
122
|
+
filename: `${personaName}-preview.yaml`,
|
|
123
|
+
renderer: ctx.renderer,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (reviewResult.aborted) {
|
|
127
|
+
ctx.showNotification("Cancelled", "info");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let previewParsed: ReturnType<typeof personaPreviewFromYAML>;
|
|
132
|
+
try {
|
|
133
|
+
previewParsed = personaPreviewFromYAML(reviewResult.content ?? previewYAML);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
ctx.showNotification(`Parse error: ${e instanceof Error ? e.message : String(e)}`, "error");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
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");
|
|
43
147
|
return;
|
|
44
148
|
}
|
|
45
|
-
|
|
149
|
+
|
|
150
|
+
if (args[0].toLowerCase() === "update") {
|
|
151
|
+
if (args.length < 3) {
|
|
152
|
+
ctx.showNotification("Usage: /p update <personaName> <personName>", "error");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const personaName = args[1];
|
|
156
|
+
const personName = args.slice(2).join(" ");
|
|
157
|
+
|
|
158
|
+
// Step 0: resolve persona (offer to create if not found)
|
|
159
|
+
let personaId = await ctx.ei.resolvePersonaName(personaName);
|
|
160
|
+
if (!personaId) {
|
|
161
|
+
const shouldCreate = await new Promise<boolean>(resolve => {
|
|
162
|
+
ctx.showOverlay((hideOverlay) => (
|
|
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();
|
|
173
|
+
}
|
|
174
|
+
const persona = await ctx.ei.getPersona(personaId);
|
|
175
|
+
if (!persona) {
|
|
176
|
+
ctx.showNotification(`Could not load persona "${personaName}"`, "error");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Step 1: find matching people
|
|
181
|
+
const human = await ctx.ei.getHuman();
|
|
182
|
+
const matches = (human.people ?? []).filter(p =>
|
|
183
|
+
p.name.toLowerCase().includes(personName.toLowerCase())
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
if (matches.length === 0) {
|
|
187
|
+
ctx.showNotification(`No person named "${personName}" in your data`, "error");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Step 2: disambiguation if multiple matches
|
|
192
|
+
let selectedPerson: typeof matches[0];
|
|
193
|
+
if (matches.length > 1) {
|
|
194
|
+
const people: PersonPickerItem[] = matches.map(p => ({
|
|
195
|
+
id: p.id,
|
|
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;
|
|
221
|
+
} else {
|
|
222
|
+
selectedPerson = matches[0];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Step 3: generate preview with loading overlay
|
|
226
|
+
let dismissed = false;
|
|
227
|
+
const overlayCallbacks2 = { hideOverlay: null as (() => void) | null, hideForEditor: null as (() => void) | null };
|
|
228
|
+
|
|
229
|
+
const previewPromise = new Promise<import('../../../src/prompts/generation/types.js').PersonaGenerationResult | null>((resolve) => {
|
|
230
|
+
ctx.showOverlay((hideOverlay, hideForEditor) => {
|
|
231
|
+
overlayCallbacks2.hideOverlay = hideOverlay;
|
|
232
|
+
overlayCallbacks2.hideForEditor = hideForEditor;
|
|
233
|
+
return (
|
|
234
|
+
<LoadingOverlay
|
|
235
|
+
message={`Generating preview for "${persona.display_name}" from "${selectedPerson.name}"...`}
|
|
236
|
+
onCancel={() => {
|
|
237
|
+
dismissed = true;
|
|
238
|
+
hideOverlay();
|
|
239
|
+
resolve(null);
|
|
240
|
+
}}
|
|
241
|
+
/>
|
|
242
|
+
);
|
|
243
|
+
}, ctx.renderer);
|
|
244
|
+
|
|
245
|
+
ctx.ei.generatePersonaPreview(
|
|
246
|
+
persona.display_name,
|
|
247
|
+
selectedPerson.description ?? '',
|
|
248
|
+
selectedPerson.relationship,
|
|
249
|
+
personaId
|
|
250
|
+
)
|
|
251
|
+
.then(result => {
|
|
252
|
+
if (!dismissed && overlayCallbacks2.hideOverlay) {
|
|
253
|
+
resolve(result);
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
.catch(e => {
|
|
257
|
+
if (!dismissed && overlayCallbacks2.hideOverlay) {
|
|
258
|
+
overlayCallbacks2.hideOverlay();
|
|
259
|
+
ctx.showNotification(`Generation failed: ${e instanceof Error ? e.message : String(e)}`, "error");
|
|
260
|
+
resolve(null);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const preview = await previewPromise;
|
|
266
|
+
if (!preview) return;
|
|
267
|
+
|
|
268
|
+
overlayCallbacks2.hideForEditor?.();
|
|
269
|
+
|
|
270
|
+
// Step 4: review editor
|
|
271
|
+
const updatePreviewYAML = personaPreviewToYAML(
|
|
272
|
+
preview,
|
|
273
|
+
persona.display_name,
|
|
274
|
+
selectedPerson.name,
|
|
275
|
+
preview.previous_long_description
|
|
276
|
+
);
|
|
277
|
+
const reviewResult = await spawnEditor({
|
|
278
|
+
initialContent: updatePreviewYAML,
|
|
279
|
+
filename: `${personaId}-update-preview.yaml`,
|
|
280
|
+
renderer: ctx.renderer,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
if (reviewResult.aborted) {
|
|
284
|
+
ctx.showNotification("Cancelled", "info");
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Step 5: parse + update
|
|
289
|
+
let previewParsed: ReturnType<typeof personaPreviewFromYAML>;
|
|
290
|
+
try {
|
|
291
|
+
previewParsed = personaPreviewFromYAML(reviewResult.content ?? updatePreviewYAML);
|
|
292
|
+
} catch (e) {
|
|
293
|
+
ctx.showNotification(`Parse error: ${e instanceof Error ? e.message : String(e)}`, "error");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
await ctx.ei.updatePersona(personaId, {
|
|
298
|
+
long_description: previewParsed.long_description,
|
|
299
|
+
short_description: previewParsed.short_description,
|
|
300
|
+
...(previewParsed.aliases ? { aliases: previewParsed.aliases } : {}),
|
|
301
|
+
traits: previewParsed.traits,
|
|
302
|
+
topics: previewParsed.topics,
|
|
303
|
+
});
|
|
304
|
+
ctx.showNotification(`Updated ${persona.display_name}`, "info");
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
46
308
|
// User typed a name - resolve it to ID, then switch
|
|
47
309
|
const nameOrAlias = args.join(" ");
|
|
48
310
|
const personaId = await ctx.ei.resolvePersonaName(nameOrAlias);
|
|
49
|
-
|
|
311
|
+
|
|
50
312
|
if (personaId) {
|
|
51
313
|
const persona = unarchived.find(p => p.id === personaId);
|
|
52
314
|
ctx.ei.selectPersona(personaId);
|