ei-tui 0.8.1 → 0.9.0
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 +6 -9
- package/tui/src/components/PromptInput.tsx +3 -1
- package/tui/src/components/RoomMessageList.tsx +2 -6
- 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
|
}
|
|
@@ -128,7 +125,7 @@ export function MessageList() {
|
|
|
128
125
|
|
|
129
126
|
const header = () => `${speaker} (${formatTime(message.timestamp)}) [✂️ ${message._quoteIndex}]:`;
|
|
130
127
|
|
|
131
|
-
const displayContent = insertQuoteMarkers(buildMessageText(message), message._quotes);
|
|
128
|
+
const displayContent = insertQuoteMarkers(buildMessageText(message, humanDisplayName()), message._quotes);
|
|
132
129
|
|
|
133
130
|
const showDivider = () => {
|
|
134
131
|
const boundary = activeContextBoundary();
|
|
@@ -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 {
|
|
@@ -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
|
|
|
@@ -14,7 +14,6 @@ interface EditableSettingsData {
|
|
|
14
14
|
default_model?: string | null;
|
|
15
15
|
oneshot_model?: string | null;
|
|
16
16
|
rewrite_model?: string | null;
|
|
17
|
-
time_mode?: "24h" | "12h" | "local" | "utc" | null;
|
|
18
17
|
name_display?: string | null;
|
|
19
18
|
default_heartbeat_ms?: string | null;
|
|
20
19
|
default_context_window_hours?: number | null;
|
|
@@ -64,7 +63,6 @@ export function settingsToYAML(settings: HumanSettings | undefined, accounts: Pr
|
|
|
64
63
|
default_model: guidToDisplay(settings?.default_model),
|
|
65
64
|
oneshot_model: guidToDisplay(settings?.oneshot_model),
|
|
66
65
|
rewrite_model: guidToDisplay(settings?.rewrite_model),
|
|
67
|
-
time_mode: settings?.time_mode ?? null,
|
|
68
66
|
name_display: settings?.name_display ?? null,
|
|
69
67
|
default_heartbeat_ms: formatDuration(settings?.default_heartbeat_ms ?? 1800000),
|
|
70
68
|
default_context_window_hours: settings?.default_context_window_hours ?? 8,
|
|
@@ -190,7 +188,6 @@ export function settingsFromYAML(yamlContent: string, original: HumanSettings |
|
|
|
190
188
|
default_model: displayToGuid(data.default_model),
|
|
191
189
|
oneshot_model: displayToGuid(data.oneshot_model),
|
|
192
190
|
rewrite_model: displayToGuid(data.rewrite_model),
|
|
193
|
-
time_mode: nullToUndefined(data.time_mode),
|
|
194
191
|
name_display: nullToUndefined(data.name_display),
|
|
195
192
|
default_heartbeat_ms: parseMsDuration(data.default_heartbeat_ms, 1800000),
|
|
196
193
|
default_context_window_hours: nullToUndefined(data.default_context_window_hours),
|