ei-tui 0.8.1 → 0.9.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 +9 -6
- package/package.json +1 -1
- package/src/core/handlers/heartbeat.ts +63 -8
- package/src/core/handlers/index.ts +2 -1
- package/src/core/handlers/persona-response.ts +3 -5
- package/src/core/handlers/rooms.ts +3 -5
- package/src/core/handlers/utils.ts +5 -4
- package/src/core/heartbeat-manager.ts +16 -47
- package/src/core/message-manager.ts +6 -2
- package/src/core/orchestrators/ceremony.ts +49 -4
- package/src/core/persona-manager.ts +1 -2
- package/src/core/personas/opencode-agent.ts +7 -2
- package/src/core/processor.ts +5 -12
- package/src/core/prompt-context-builder.ts +11 -1
- package/src/core/queue-processor.ts +4 -4
- package/src/core/room-manager.ts +6 -6
- package/src/core/state/human.ts +0 -1
- package/src/core/state/personas.ts +22 -13
- package/src/core/state/rooms.ts +0 -2
- package/src/core/state-manager.ts +83 -11
- package/src/core/types/data-items.ts +2 -2
- package/src/core/types/entities.ts +8 -3
- package/src/core/types/enums.ts +1 -0
- package/src/core/types/integrations.ts +1 -1
- package/src/core/types/llm.ts +2 -4
- package/src/core/types/rooms.ts +0 -4
- package/src/integrations/claude-code/importer.ts +1 -5
- package/src/integrations/cursor/importer.ts +1 -5
- package/src/integrations/opencode/importer.ts +1 -4
- package/src/integrations/opencode/types.ts +17 -1
- package/src/prompts/heartbeat/check.ts +7 -18
- package/src/prompts/heartbeat/ei.ts +14 -0
- package/src/prompts/heartbeat/types.ts +7 -5
- package/src/prompts/index.ts +9 -0
- package/src/prompts/message-utils.ts +7 -4
- package/src/prompts/reflection/index.ts +77 -0
- package/src/prompts/reflection/types.ts +26 -0
- package/src/prompts/response/index.ts +5 -2
- package/src/prompts/response/sections.ts +29 -1
- package/src/prompts/response/types.ts +10 -2
- package/src/prompts/room/sections.ts +4 -7
- package/src/prompts/room/types.ts +3 -6
- package/src/storage/embeddings.ts +69 -34
- package/src/storage/merge.ts +1 -1
- package/src/templates/welcome.ts +0 -1
- package/tui/README.md +5 -2
- package/tui/src/commands/editor.tsx +0 -1
- package/tui/src/commands/persona.tsx +89 -3
- package/tui/src/commands/reflect.tsx +375 -0
- package/tui/src/commands/registry.ts +2 -0
- package/tui/src/components/CYPTreeOverlay.tsx +0 -2
- package/tui/src/components/MAPScoreOverlay.tsx +1 -1
- package/tui/src/components/MessageList.tsx +8 -10
- package/tui/src/components/PromptInput.tsx +3 -1
- package/tui/src/components/RoomMessageList.tsx +5 -8
- package/tui/src/components/Sidebar.tsx +3 -5
- package/tui/src/components/StatusBar.tsx +26 -14
- package/tui/src/context/keyboard.tsx +2 -2
- package/tui/src/util/cyp-editor.tsx +2 -6
- package/tui/src/util/yaml-context.ts +2 -6
- package/tui/src/util/yaml-persona.ts +3 -3
- package/tui/src/util/yaml-settings.ts +0 -3
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { useKeyboard } from "@opentui/solid";
|
|
2
|
+
import { onMount, onCleanup } from "solid-js";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import type { Command, CommandContext } from "./registry";
|
|
7
|
+
import type { PersonaEntity } from "../../../src/core/types.js";
|
|
8
|
+
import { ConfirmOverlay } from "../components/ConfirmOverlay.js";
|
|
9
|
+
import {
|
|
10
|
+
personaPreviewToYAML,
|
|
11
|
+
personaPreviewFromYAML,
|
|
12
|
+
} from "../util/yaml-serializers.js";
|
|
13
|
+
import { useKeyboardNav } from "../context/keyboard.js";
|
|
14
|
+
|
|
15
|
+
function wrapComment(text: string, width = 78): string {
|
|
16
|
+
const words = text.split(/\s+/);
|
|
17
|
+
const lines: string[] = [];
|
|
18
|
+
let current = "";
|
|
19
|
+
for (const word of words) {
|
|
20
|
+
if (current && (current + " " + word).length > width) {
|
|
21
|
+
lines.push(`# ${current}`);
|
|
22
|
+
current = word;
|
|
23
|
+
} else {
|
|
24
|
+
current = current ? `${current} ${word}` : word;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (current) lines.push(`# ${current}`);
|
|
28
|
+
return lines.join("\n");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getDataPath(): string {
|
|
32
|
+
const raw = process.env.EI_DATA_PATH ??
|
|
33
|
+
join(process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"), "ei");
|
|
34
|
+
return raw.replace(/\/+$/, "");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getReflectFolder(persona: PersonaEntity): string {
|
|
38
|
+
const datePrefix = persona.pending_update!.created_at.slice(0, 10);
|
|
39
|
+
const safeName = persona.display_name.replace(/\s+/g, "_");
|
|
40
|
+
return join(getDataPath(), "reflect", `${datePrefix}_${safeName}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function resolveReflectionPersona(
|
|
44
|
+
args: string[],
|
|
45
|
+
ctx: CommandContext
|
|
46
|
+
): Promise<{ personaId: string; persona: PersonaEntity } | null> {
|
|
47
|
+
const SUBCOMMANDS = new Set(["generate", "update", "apply", "dismiss"]);
|
|
48
|
+
const nameArgs = SUBCOMMANDS.has(args[0]?.toLowerCase() ?? "") ? args.slice(1) : args;
|
|
49
|
+
const name = nameArgs.join(" ").trim();
|
|
50
|
+
|
|
51
|
+
let personaId: string | null;
|
|
52
|
+
if (name) {
|
|
53
|
+
personaId = await ctx.ei.resolvePersonaName(name);
|
|
54
|
+
if (!personaId) {
|
|
55
|
+
ctx.showNotification(`No persona named "${name}"`, "error");
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
personaId = ctx.ei.activePersonaId();
|
|
60
|
+
if (!personaId) {
|
|
61
|
+
ctx.showNotification("No active persona", "error");
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const persona = await ctx.ei.getPersona(personaId);
|
|
67
|
+
if (!persona) {
|
|
68
|
+
ctx.showNotification("Could not load persona", "error");
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!persona.pending_update) {
|
|
73
|
+
ctx.showNotification(`No pending reflection for ${persona.display_name}`, "error");
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { personaId, persona };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const reflectCommand: Command = {
|
|
81
|
+
name: "reflect",
|
|
82
|
+
aliases: ["rf"],
|
|
83
|
+
description:
|
|
84
|
+
"Review a persona's pending reflection — compare current vs proposed identity while chatting",
|
|
85
|
+
usage: "/reflect [generate|update|apply|dismiss]",
|
|
86
|
+
|
|
87
|
+
async execute(args, ctx) {
|
|
88
|
+
const subcommand = args[0]?.toLowerCase();
|
|
89
|
+
|
|
90
|
+
if (!subcommand || !["generate", "update", "apply", "dismiss"].includes(subcommand)) {
|
|
91
|
+
const activeId = ctx.ei.activePersonaId();
|
|
92
|
+
const activePersona = activeId ? ctx.ei.personas().find(p => p.id === activeId) : null;
|
|
93
|
+
const hasPending = activePersona?.has_pending_update ?? false;
|
|
94
|
+
const pendingPersonas = hasPending
|
|
95
|
+
? []
|
|
96
|
+
: ctx.ei.personas().filter(p => p.has_pending_update);
|
|
97
|
+
|
|
98
|
+
if (!hasPending && pendingPersonas.length === 0) {
|
|
99
|
+
ctx.showNotification("No pending reflections.", "info");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const headerName = hasPending ? activePersona!.display_name : undefined;
|
|
104
|
+
const pendingNames = pendingPersonas.map(p => p.display_name);
|
|
105
|
+
const dataPath = getDataPath();
|
|
106
|
+
|
|
107
|
+
ctx.showOverlay((hideOverlay, _hideForEditor) => {
|
|
108
|
+
const { setOverlayActive } = useKeyboardNav();
|
|
109
|
+
onMount(() => setOverlayActive(true));
|
|
110
|
+
onCleanup(() => setOverlayActive(false));
|
|
111
|
+
useKeyboard((event) => {
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
hideOverlay();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const header = headerName
|
|
117
|
+
? `✦ Persona Reflection: ${headerName}`
|
|
118
|
+
: `✦ Persona Reflection — pending: ${pendingNames.join(", ")}`;
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<box
|
|
122
|
+
position="absolute"
|
|
123
|
+
width="100%"
|
|
124
|
+
height="100%"
|
|
125
|
+
left={0}
|
|
126
|
+
top={0}
|
|
127
|
+
backgroundColor="#000000"
|
|
128
|
+
alignItems="center"
|
|
129
|
+
justifyContent="center"
|
|
130
|
+
>
|
|
131
|
+
<box
|
|
132
|
+
width={64}
|
|
133
|
+
backgroundColor="#1a1a2e"
|
|
134
|
+
borderStyle="single"
|
|
135
|
+
borderColor="#586e75"
|
|
136
|
+
padding={2}
|
|
137
|
+
flexDirection="column"
|
|
138
|
+
>
|
|
139
|
+
<text fg="#eee8d5">{header}</text>
|
|
140
|
+
<text> </text>
|
|
141
|
+
<text fg="#839496">{"Personas grow through conversation. Reflection is a chance"}</text>
|
|
142
|
+
<text fg="#839496">{"to review proposed changes to their Identity with them."}</text>
|
|
143
|
+
<text fg="#839496">{"Open the generated files in your editor while you chat."}</text>
|
|
144
|
+
<text> </text>
|
|
145
|
+
<text fg="#268bd2">{" /reflect generate Write current + proposed YAML files"}</text>
|
|
146
|
+
<text fg="#268bd2">{" /reflect update Read proposed.yaml back into Ei"}</text>
|
|
147
|
+
<text fg="#268bd2">{" (Persona sees updated data)"}</text>
|
|
148
|
+
<text fg="#268bd2">{" /reflect apply Write proposed.yaml to your Persona"}</text>
|
|
149
|
+
<text fg="#268bd2">{" /reflect dismiss Discard without changing anything"}</text>
|
|
150
|
+
<text> </text>
|
|
151
|
+
<text fg="#839496">{dataPath.replace(homedir(), "~") + "/reflect/YYYY-MM-DD_Name/"}</text>
|
|
152
|
+
<text fg="#839496">{" ├── current.yaml # Read-Only Reference"}</text>
|
|
153
|
+
<text fg="#839496">{" └── proposed.yaml # Edit while Chatting"}</text>
|
|
154
|
+
<text> </text>
|
|
155
|
+
<text fg="#586e75">{"[Press any key to close]"}</text>
|
|
156
|
+
</box>
|
|
157
|
+
</box>
|
|
158
|
+
);
|
|
159
|
+
}, ctx.renderer);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (subcommand === "generate") {
|
|
164
|
+
const result = await resolveReflectionPersona(args, ctx);
|
|
165
|
+
if (!result) return;
|
|
166
|
+
const { persona } = result;
|
|
167
|
+
|
|
168
|
+
const folderPath = getReflectFolder(persona);
|
|
169
|
+
|
|
170
|
+
if (fs.existsSync(folderPath)) {
|
|
171
|
+
const date = persona.pending_update!.created_at.slice(0, 10);
|
|
172
|
+
ctx.showNotification(`Found existing reflection from ${date}, regenerating...`, "info");
|
|
173
|
+
fs.rmSync(folderPath, { recursive: true, force: true });
|
|
174
|
+
}
|
|
175
|
+
fs.mkdirSync(folderPath, { recursive: true });
|
|
176
|
+
|
|
177
|
+
const now = new Date().toISOString();
|
|
178
|
+
const critique = wrapComment(persona.pending_update!.critique);
|
|
179
|
+
|
|
180
|
+
const currentHeader = [
|
|
181
|
+
`# Current Identity: ${persona.display_name}`,
|
|
182
|
+
`# This is the CURRENT identity — for reference only. Do not edit this file.`,
|
|
183
|
+
`# Generated: ${now}`,
|
|
184
|
+
`#`,
|
|
185
|
+
`# Critique from reflection analysis:`,
|
|
186
|
+
critique,
|
|
187
|
+
``,
|
|
188
|
+
].join("\n");
|
|
189
|
+
|
|
190
|
+
const currentYAML = personaPreviewToYAML(
|
|
191
|
+
{
|
|
192
|
+
long_description: persona.long_description ?? "",
|
|
193
|
+
short_description: persona.short_description ?? "",
|
|
194
|
+
traits: (persona.traits ?? []).map(t => ({
|
|
195
|
+
name: t.name,
|
|
196
|
+
description: t.description,
|
|
197
|
+
strength: t.strength ?? 0.5,
|
|
198
|
+
sentiment: t.sentiment ?? 0,
|
|
199
|
+
})),
|
|
200
|
+
topics: (persona.topics ?? []).map(t => ({
|
|
201
|
+
name: t.name,
|
|
202
|
+
perspective: t.perspective,
|
|
203
|
+
approach: t.approach,
|
|
204
|
+
personal_stake: t.personal_stake,
|
|
205
|
+
sentiment: t.sentiment ?? 0,
|
|
206
|
+
exposure_current: t.exposure_current ?? 0.5,
|
|
207
|
+
exposure_desired: t.exposure_desired ?? 0.5,
|
|
208
|
+
})),
|
|
209
|
+
},
|
|
210
|
+
persona.display_name
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
fs.writeFileSync(join(folderPath, "current.yaml"), currentHeader + currentYAML, "utf-8");
|
|
214
|
+
|
|
215
|
+
const proposedHeader = [
|
|
216
|
+
`# Proposed Identity: ${persona.display_name}`,
|
|
217
|
+
`# Edit this file freely. Use /reflect update to sync changes into the system.`,
|
|
218
|
+
`# Use /reflect apply when done, or /reflect dismiss to discard.`,
|
|
219
|
+
``,
|
|
220
|
+
].join("\n");
|
|
221
|
+
|
|
222
|
+
const proposedYAML = personaPreviewToYAML(
|
|
223
|
+
{
|
|
224
|
+
long_description: persona.pending_update!.long_description,
|
|
225
|
+
short_description: persona.pending_update!.short_description ?? "",
|
|
226
|
+
traits: (persona.pending_update!.traits ?? []).map(t => ({
|
|
227
|
+
name: t.name,
|
|
228
|
+
description: t.description,
|
|
229
|
+
strength: t.strength ?? 0.5,
|
|
230
|
+
sentiment: t.sentiment ?? 0,
|
|
231
|
+
})),
|
|
232
|
+
topics: (persona.pending_update!.topics ?? []).map(t => ({
|
|
233
|
+
name: t.name,
|
|
234
|
+
perspective: t.perspective,
|
|
235
|
+
approach: t.approach,
|
|
236
|
+
personal_stake: t.personal_stake,
|
|
237
|
+
sentiment: t.sentiment ?? 0,
|
|
238
|
+
exposure_current: t.exposure_current ?? 0.5,
|
|
239
|
+
exposure_desired: t.exposure_desired ?? 0.5,
|
|
240
|
+
})),
|
|
241
|
+
},
|
|
242
|
+
persona.display_name
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
fs.writeFileSync(join(folderPath, "proposed.yaml"), proposedHeader + proposedYAML, "utf-8");
|
|
246
|
+
|
|
247
|
+
ctx.showNotification(
|
|
248
|
+
`Reflection files written to ${folderPath} — open proposed.yaml in your editor alongside this chat`,
|
|
249
|
+
"info"
|
|
250
|
+
);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (subcommand === "update") {
|
|
255
|
+
const result = await resolveReflectionPersona(args, ctx);
|
|
256
|
+
if (!result) return;
|
|
257
|
+
const { personaId, persona } = result;
|
|
258
|
+
|
|
259
|
+
const folderPath = getReflectFolder(persona);
|
|
260
|
+
if (!fs.existsSync(folderPath)) {
|
|
261
|
+
ctx.showNotification(
|
|
262
|
+
`No reflection folder found — run /reflect generate first`,
|
|
263
|
+
"error"
|
|
264
|
+
);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const proposedContent = fs.readFileSync(join(folderPath, "proposed.yaml"), "utf-8");
|
|
269
|
+
|
|
270
|
+
let parsed: ReturnType<typeof personaPreviewFromYAML>;
|
|
271
|
+
try {
|
|
272
|
+
parsed = personaPreviewFromYAML(proposedContent);
|
|
273
|
+
} catch (e) {
|
|
274
|
+
ctx.showNotification(
|
|
275
|
+
`Parse error: ${e instanceof Error ? e.message : String(e)}`,
|
|
276
|
+
"error"
|
|
277
|
+
);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
await ctx.ei.updatePersona(personaId, {
|
|
282
|
+
pending_update: {
|
|
283
|
+
critique: persona.pending_update!.critique,
|
|
284
|
+
created_at: persona.pending_update!.created_at,
|
|
285
|
+
long_description: parsed.long_description,
|
|
286
|
+
short_description: parsed.short_description ?? persona.pending_update!.short_description,
|
|
287
|
+
traits: parsed.traits,
|
|
288
|
+
topics: parsed.topics,
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
ctx.showNotification(
|
|
293
|
+
`Updated — ${persona.display_name} will see your changes in the next message`,
|
|
294
|
+
"info"
|
|
295
|
+
);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (subcommand === "apply") {
|
|
300
|
+
const result = await resolveReflectionPersona(args, ctx);
|
|
301
|
+
if (!result) return;
|
|
302
|
+
const { personaId, persona } = result;
|
|
303
|
+
|
|
304
|
+
const folderPath = getReflectFolder(persona);
|
|
305
|
+
|
|
306
|
+
let finalParsed: ReturnType<typeof personaPreviewFromYAML> | null = null;
|
|
307
|
+
if (fs.existsSync(folderPath)) {
|
|
308
|
+
const proposedPath = join(folderPath, "proposed.yaml");
|
|
309
|
+
if (fs.existsSync(proposedPath)) {
|
|
310
|
+
const proposedContent = fs.readFileSync(proposedPath, "utf-8");
|
|
311
|
+
try {
|
|
312
|
+
finalParsed = personaPreviewFromYAML(proposedContent);
|
|
313
|
+
} catch (e) {
|
|
314
|
+
ctx.showNotification(
|
|
315
|
+
`Parse error in proposed.yaml: ${e instanceof Error ? e.message : String(e)}`,
|
|
316
|
+
"error"
|
|
317
|
+
);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const source = finalParsed ?? {
|
|
324
|
+
long_description: persona.pending_update!.long_description,
|
|
325
|
+
short_description: persona.pending_update!.short_description as string | undefined,
|
|
326
|
+
traits: persona.pending_update!.traits,
|
|
327
|
+
topics: persona.pending_update!.topics,
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
await ctx.ei.updatePersona(personaId, {
|
|
331
|
+
long_description: source.long_description,
|
|
332
|
+
short_description: source.short_description,
|
|
333
|
+
traits: source.traits,
|
|
334
|
+
topics: source.topics,
|
|
335
|
+
pending_update: undefined,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
if (fs.existsSync(folderPath)) {
|
|
339
|
+
fs.rmSync(folderPath, { recursive: true, force: true });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
ctx.showNotification(`Applied reflection for ${persona.display_name}`, "info");
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (subcommand === "dismiss") {
|
|
347
|
+
const result = await resolveReflectionPersona(args, ctx);
|
|
348
|
+
if (!result) return;
|
|
349
|
+
const { personaId, persona } = result;
|
|
350
|
+
|
|
351
|
+
const confirmed = await new Promise<boolean>((resolve) => {
|
|
352
|
+
ctx.showOverlay((hideOverlay, _hideForEditor) => (
|
|
353
|
+
<ConfirmOverlay
|
|
354
|
+
message={`Discard this reflection for ${persona.display_name}? The proposed identity will be lost.`}
|
|
355
|
+
onConfirm={() => { hideOverlay(); resolve(true); }}
|
|
356
|
+
onCancel={() => { hideOverlay(); resolve(false); }}
|
|
357
|
+
/>
|
|
358
|
+
), ctx.renderer);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
if (!confirmed) {
|
|
362
|
+
ctx.showNotification("Cancelled", "info");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
await ctx.ei.updatePersona(personaId, { pending_update: undefined });
|
|
367
|
+
const folderPath = getReflectFolder(persona);
|
|
368
|
+
if (fs.existsSync(folderPath)) {
|
|
369
|
+
fs.rmSync(folderPath, { recursive: true, force: true });
|
|
370
|
+
}
|
|
371
|
+
ctx.showNotification(`Dismissed reflection for ${persona.display_name}`, "info");
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OverlayRenderer } from "../context/overlay";
|
|
2
2
|
import type { EiContextValue } from "../context/ei";
|
|
3
3
|
import type { CliRenderer } from "@opentui/core";
|
|
4
|
+
import { logger } from "../util/logger";
|
|
4
5
|
|
|
5
6
|
export interface Command {
|
|
6
7
|
name: string;
|
|
@@ -116,6 +117,7 @@ export async function parseAndExecute(
|
|
|
116
117
|
await command.execute(args, ctx);
|
|
117
118
|
} catch (error) {
|
|
118
119
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
120
|
+
logger.error(`[command:${commandName}] ${errorMsg}`, error);
|
|
119
121
|
ctx.showNotification(`Command failed: ${errorMsg}`, "error");
|
|
120
122
|
}
|
|
121
123
|
|
|
@@ -31,8 +31,6 @@ interface TreeLine {
|
|
|
31
31
|
|
|
32
32
|
function getMessageContent(m: RoomMessage): string {
|
|
33
33
|
if (m.content) return m.content;
|
|
34
|
-
if (m.verbal_response) return m.verbal_response;
|
|
35
|
-
if (m.action_response) return m.action_response;
|
|
36
34
|
if (m.silence_reason) return `(${m.silence_reason})`;
|
|
37
35
|
return "";
|
|
38
36
|
}
|
|
@@ -32,7 +32,7 @@ function truncate(s: string, n: number): string {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
function getMessageText(m: RoomMessage): string {
|
|
35
|
-
return (m.
|
|
35
|
+
return (m.content ?? m.silence_reason ?? "").replace(/\n+/g, " ");
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export function MAPScoreOverlay(props: MAPScoreOverlayProps) {
|
|
@@ -19,17 +19,14 @@ function formatTime(timestamp: string): string {
|
|
|
19
19
|
return `${hours}:${minutes}`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
function getContent(msg: { content?: string
|
|
23
|
-
|
|
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');
|
|
22
|
+
function getContent(msg: { content?: string }): string {
|
|
23
|
+
return msg.content ?? '';
|
|
28
24
|
}
|
|
29
25
|
|
|
30
|
-
function buildMessageText(message: Message): string {
|
|
26
|
+
function buildMessageText(message: Message, humanName: string): string {
|
|
31
27
|
if (message.silence_reason !== undefined) {
|
|
32
|
-
|
|
28
|
+
const silentParty = message.role === "human" ? humanName : "Persona";
|
|
29
|
+
return `[${silentParty} chose not to respond: ${message.silence_reason}]`;
|
|
33
30
|
}
|
|
34
31
|
return getContent(message);
|
|
35
32
|
}
|
|
@@ -116,6 +113,7 @@ export function MessageList() {
|
|
|
116
113
|
stickyScroll={true}
|
|
117
114
|
stickyStart="bottom"
|
|
118
115
|
viewportCulling={true}
|
|
116
|
+
wrapperOptions={{ paddingRight: 2 }}
|
|
119
117
|
>
|
|
120
118
|
<For each={messagesWithQuotes()}>
|
|
121
119
|
{(message, index) => {
|
|
@@ -128,7 +126,7 @@ export function MessageList() {
|
|
|
128
126
|
|
|
129
127
|
const header = () => `${speaker} (${formatTime(message.timestamp)}) [✂️ ${message._quoteIndex}]:`;
|
|
130
128
|
|
|
131
|
-
const displayContent = insertQuoteMarkers(buildMessageText(message), message._quotes);
|
|
129
|
+
const displayContent = insertQuoteMarkers(buildMessageText(message, humanDisplayName()), message._quotes);
|
|
132
130
|
|
|
133
131
|
const showDivider = () => {
|
|
134
132
|
const boundary = activeContextBoundary();
|
|
@@ -162,7 +160,7 @@ export function MessageList() {
|
|
|
162
160
|
attributes={TextAttributes.BOLD}
|
|
163
161
|
content={header()}
|
|
164
162
|
/>
|
|
165
|
-
<box
|
|
163
|
+
<box paddingLeft={2} flexGrow={1}>
|
|
166
164
|
<markdown
|
|
167
165
|
content={displayContent}
|
|
168
166
|
syntaxStyle={solarizedDarkSyntax}
|
|
@@ -27,6 +27,7 @@ import { authCommand } from '../commands/auth';
|
|
|
27
27
|
import { dedupeCommand } from "../commands/dedupe";
|
|
28
28
|
import { roomCommand } from "../commands/room.js";
|
|
29
29
|
import { activateCommand } from "../commands/activate.js";
|
|
30
|
+
import { reflectCommand } from "../commands/reflect.js";
|
|
30
31
|
import { silenceCommand } from "../commands/silence.js";
|
|
31
32
|
import { captureCommand } from "../commands/capture.js";
|
|
32
33
|
import { openCYPEditor } from "../util/cyp-editor.js";
|
|
@@ -90,6 +91,7 @@ export function PromptInput() {
|
|
|
90
91
|
registerCommand(resumeCommand);
|
|
91
92
|
registerCommand(archiveCommand);
|
|
92
93
|
registerCommand(unarchiveCommand);
|
|
94
|
+
registerCommand(reflectCommand);
|
|
93
95
|
|
|
94
96
|
let textareaRef: TextareaRenderable | undefined;
|
|
95
97
|
|
|
@@ -164,7 +166,7 @@ export function PromptInput() {
|
|
|
164
166
|
);
|
|
165
167
|
const recalled = recallHumanRoomMessage();
|
|
166
168
|
if (recalled) {
|
|
167
|
-
const content = pendingMsg?.
|
|
169
|
+
const content = pendingMsg?.content ?? pendingMsg?.silence_reason ?? "";
|
|
168
170
|
textareaRef?.setText(content);
|
|
169
171
|
setInputText(content);
|
|
170
172
|
textareaRef?.gotoBufferEnd();
|
|
@@ -7,12 +7,8 @@ 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
|
|
11
|
-
|
|
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');
|
|
10
|
+
function getContent(msg: { content?: string }): string {
|
|
11
|
+
return msg.content ?? '';
|
|
16
12
|
}
|
|
17
13
|
|
|
18
14
|
interface RoomMessageWithQuotes extends RoomMessage {
|
|
@@ -147,6 +143,7 @@ export function RoomMessageList() {
|
|
|
147
143
|
stickyScroll={true}
|
|
148
144
|
stickyStart="bottom"
|
|
149
145
|
viewportCulling={true}
|
|
146
|
+
wrapperOptions={{ paddingRight: 2 }}
|
|
150
147
|
>
|
|
151
148
|
<For each={displayMessagesWithQuotes()}>
|
|
152
149
|
{(msg) => {
|
|
@@ -176,10 +173,10 @@ export function RoomMessageList() {
|
|
|
176
173
|
attributes={TextAttributes.BOLD}
|
|
177
174
|
content={header}
|
|
178
175
|
/>
|
|
179
|
-
<box
|
|
176
|
+
<box paddingLeft={2} flexGrow={1} visible={isSilence}>
|
|
180
177
|
<text fg="#586e75" content={silenceText} />
|
|
181
178
|
</box>
|
|
182
|
-
<box
|
|
179
|
+
<box paddingLeft={2} flexGrow={1} visible={!isSilence}>
|
|
183
180
|
<markdown
|
|
184
181
|
content={normalContent}
|
|
185
182
|
syntaxStyle={solarizedDarkSyntax}
|
|
@@ -25,11 +25,8 @@ export function Sidebar() {
|
|
|
25
25
|
personas().filter(p => !p.is_archived)
|
|
26
26
|
);
|
|
27
27
|
|
|
28
|
-
// Memoize visible (non-archived) rooms sorted by last_activity desc
|
|
29
28
|
const visibleRooms = createMemo(() =>
|
|
30
|
-
rooms()
|
|
31
|
-
.filter((r: RoomSummary) => !r.is_archived)
|
|
32
|
-
.sort((a: RoomSummary, b: RoomSummary) => b.last_activity.localeCompare(a.last_activity))
|
|
29
|
+
rooms().filter((r: RoomSummary) => !r.is_archived)
|
|
33
30
|
);
|
|
34
31
|
|
|
35
32
|
const [highlightedPersona, setHighlightedPersona] = createSignal<string | null>(null);
|
|
@@ -79,7 +76,8 @@ export function Sidebar() {
|
|
|
79
76
|
const name = displayName();
|
|
80
77
|
const unread = persona.unread_count > 0 ? ` (${persona.unread_count} new)` : "";
|
|
81
78
|
const paused = persona.is_paused ? " ⏸" : "";
|
|
82
|
-
|
|
79
|
+
const reflect = persona.has_pending_update ? " ✦" : "";
|
|
80
|
+
return `${prefix}${name}${unread}${paused}${reflect}`;
|
|
83
81
|
};
|
|
84
82
|
|
|
85
83
|
const textColor = () => {
|
|
@@ -133,22 +133,34 @@ export function StatusBar() {
|
|
|
133
133
|
>
|
|
134
134
|
<box flexGrow={1}>
|
|
135
135
|
<Show when={notification()} fallback={
|
|
136
|
-
<Show
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
136
|
+
<Show
|
|
137
|
+
when={(() => {
|
|
138
|
+
const id = activePersonaId();
|
|
139
|
+
return id ? personas().find(p => p.id === id)?.has_pending_update : false;
|
|
140
|
+
})()}
|
|
141
|
+
fallback={
|
|
142
|
+
<Show when={activeRoomId()} fallback={
|
|
143
|
+
<text fg="#586e75">
|
|
144
|
+
<Show when={getActiveDisplayName()} fallback="No persona selected">
|
|
145
|
+
{getActiveDisplayName()}
|
|
146
|
+
</Show>
|
|
147
|
+
</text>
|
|
148
|
+
}>
|
|
149
|
+
<Show when={allPersonasResponded() && humanRoomMessagePending()} fallback={
|
|
150
|
+
<text fg="#586e75">
|
|
151
|
+
{getRoomWaitingText()}
|
|
152
|
+
</text>
|
|
153
|
+
}>
|
|
154
|
+
<text fg="#586e75">
|
|
155
|
+
{activeRoom()?.display_name ?? ""}
|
|
156
|
+
</text>
|
|
157
|
+
</Show>
|
|
140
158
|
</Show>
|
|
159
|
+
}
|
|
160
|
+
>
|
|
161
|
+
<text fg="#b58900">
|
|
162
|
+
{`✦ Reflection ready for ${getActiveDisplayName()} — /reflect to learn more`}
|
|
141
163
|
</text>
|
|
142
|
-
}>
|
|
143
|
-
<Show when={allPersonasResponded() && humanRoomMessagePending()} fallback={
|
|
144
|
-
<text fg="#586e75">
|
|
145
|
-
{getRoomWaitingText()}
|
|
146
|
-
</text>
|
|
147
|
-
}>
|
|
148
|
-
<text fg="#586e75">
|
|
149
|
-
{activeRoom()?.display_name ?? ""}
|
|
150
|
-
</text>
|
|
151
|
-
</Show>
|
|
152
164
|
</Show>
|
|
153
165
|
}>
|
|
154
166
|
<text fg={getNotificationColor()}>
|
|
@@ -178,7 +178,7 @@ export const KeyboardProvider: ParentComponent = (props) => {
|
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
180
|
// Navigate backward through sent-message history
|
|
181
|
-
const history = messages().filter(m => m.role === "human").map(m => (m.content ??
|
|
181
|
+
const history = messages().filter(m => m.role === "human").map(m => (m.content ?? ''));
|
|
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
|
-
const history = messages().filter(m => m.role === "human").map(m => (m.content ??
|
|
209
|
+
const history = messages().filter(m => m.role === "human").map(m => (m.content ?? ''));
|
|
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,12 +7,8 @@ interface ParsedBlock {
|
|
|
7
7
|
chosen: boolean;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
function getContent(m: { content?: string
|
|
11
|
-
|
|
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');
|
|
10
|
+
function getContent(m: { content?: string }): string {
|
|
11
|
+
return m.content ?? '';
|
|
16
12
|
}
|
|
17
13
|
|
|
18
14
|
export function buildCYPEditorYAML(
|
|
@@ -12,12 +12,8 @@ interface EditableMessage {
|
|
|
12
12
|
silence_reason?: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
function getContent(m: { content?: string
|
|
16
|
-
|
|
17
|
-
const parts: string[] = [];
|
|
18
|
-
if (m.action_response) parts.push(`_${m.action_response}_`);
|
|
19
|
-
if (m.verbal_response) parts.push(m.verbal_response);
|
|
20
|
-
return parts.join('\n\n');
|
|
15
|
+
function getContent(m: { content?: string }): string {
|
|
16
|
+
return m.content ?? '';
|
|
21
17
|
}
|
|
22
18
|
|
|
23
19
|
export function contextToYAML(messages: Message[]): string {
|
|
@@ -214,15 +214,15 @@ export function personaToYAML(persona: PersonaEntity, allGroups?: string[], allT
|
|
|
214
214
|
: persona.traits.map(({ name, description, sentiment, strength }) => ({ name, description, sentiment: sentiment ?? 0, strength: strength ?? 0.5 })),
|
|
215
215
|
topics: useTopicPlaceholder
|
|
216
216
|
? [PLACEHOLDER_TOPIC]
|
|
217
|
-
: persona.topics.map(({ name, perspective, approach, personal_stake, exposure_current, exposure_desired }) => ({
|
|
218
|
-
name, perspective, approach, personal_stake, exposure_current, exposure_desired
|
|
217
|
+
: persona.topics.map(({ name, perspective, approach, personal_stake, sentiment, exposure_current, exposure_desired }) => ({
|
|
218
|
+
name, perspective, approach, personal_stake, sentiment: sentiment ?? 0, exposure_current, exposure_desired
|
|
219
219
|
})),
|
|
220
220
|
heartbeat_delay_ms: persona.heartbeat_delay_ms ? formatDuration(persona.heartbeat_delay_ms) : null,
|
|
221
221
|
context_window_hours: persona.context_window_hours ?? null,
|
|
222
222
|
is_paused: persona.is_paused || undefined,
|
|
223
223
|
pause_until: persona.pause_until,
|
|
224
224
|
is_static: persona.is_static || undefined,
|
|
225
|
-
include_message_timestamps: persona.include_message_timestamps
|
|
225
|
+
include_message_timestamps: persona.include_message_timestamps ?? false,
|
|
226
226
|
tools: toolsMap,
|
|
227
227
|
};
|
|
228
228
|
|