pi-incarnate 0.1.0 → 0.2.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/CHANGELOG.md +27 -0
- package/README.md +113 -21
- package/SECURITY.md +10 -3
- package/characters/README.md +5 -1
- package/characters/mira/CHARACTER.md +1 -1
- package/evals/persona-scenarios.json +104 -0
- package/extensions/index.ts +34 -17
- package/package.json +5 -1
- package/scripts/persona-eval.mjs +211 -0
- package/src/avatar-manager.ts +156 -0
- package/src/avatar.ts +93 -22
- package/src/character-bundle.ts +330 -0
- package/src/character-catalog.ts +69 -0
- package/src/character-editor.ts +199 -0
- package/src/character-lifecycle.ts +94 -0
- package/src/character-loader.ts +36 -12
- package/src/character-section-editor.ts +120 -0
- package/src/commands.ts +45 -20
- package/src/form-editor.ts +115 -0
- package/src/markdown.ts +12 -0
- package/src/menu.ts +752 -0
- package/src/mood.ts +1 -1
- package/src/persona.ts +3 -1
- package/src/select-menu.ts +70 -0
- package/src/session-state.ts +17 -5
- package/src/ui-protocol.ts +66 -0
package/src/mood.ts
CHANGED
|
@@ -50,5 +50,5 @@ export function composeMoodPrompt(config: MoodConfig, presetId: string | undefin
|
|
|
50
50
|
if (!presetId) return undefined;
|
|
51
51
|
const preset = config.presets.get(presetId);
|
|
52
52
|
if (!preset) return undefined;
|
|
53
|
-
return `Current mood preset: ${preset.id}\n${preset.instruction}\nThe mood adjusts expression only; it does not override the character's identity, factual standards, or tool rules.`;
|
|
53
|
+
return `Current mood preset: ${preset.id}\nOnly this named preset is active; do not blend in other preset definitions from the character card.\n${preset.instruction}\nThe response must visibly demonstrate this preset; merely remaining compatible with it is not enough. Make it perceptible in wording, pacing, and response strategy without exaggerating it.\nThe mood adjusts expression only; it does not override the character's identity, factual standards, or tool rules.`;
|
|
54
54
|
}
|
package/src/persona.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Character } from "./character-loader.ts";
|
|
2
2
|
import { composeFormsPrompt } from "./forms.ts";
|
|
3
|
+
import { removeLevelTwoSection } from "./markdown.ts";
|
|
3
4
|
import { composeMoodPrompt } from "./mood.ts";
|
|
4
5
|
|
|
5
6
|
const RUNTIME_RULES = `You are operating with a pi-incarnate character persona.
|
|
@@ -13,12 +14,13 @@ const RUNTIME_RULES = `You are operating with a pi-incarnate character persona.
|
|
|
13
14
|
export function composePersonaPrompt(character: Character, currentMood?: string): string {
|
|
14
15
|
const formsPrompt = composeFormsPrompt(character.forms);
|
|
15
16
|
const moodPrompt = composeMoodPrompt(character.mood, currentMood);
|
|
17
|
+
const runtimeCard = removeLevelTwoSection(character.markdown, "Current Mood");
|
|
16
18
|
return `${RUNTIME_RULES}
|
|
17
19
|
|
|
18
20
|
Active character: ${character.name} (${character.id})
|
|
19
21
|
|
|
20
22
|
<character-card>
|
|
21
|
-
${
|
|
23
|
+
${runtimeCard}
|
|
22
24
|
</character-card>${formsPrompt ? `\n\n${formsPrompt}` : ""}${moodPrompt ? `\n\n${moodPrompt}` : ""}`;
|
|
23
25
|
}
|
|
24
26
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Container, SelectList, Text, truncateToWidth, type TUI } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
function supportsThemedMenu(ctx: ExtensionCommandContext): boolean {
|
|
5
|
+
return typeof ctx.ui.custom === "function" && ctx.ui.theme !== undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function menuComponent(
|
|
9
|
+
tui: TUI,
|
|
10
|
+
theme: Theme,
|
|
11
|
+
title: string,
|
|
12
|
+
options: readonly string[],
|
|
13
|
+
done: (result: string | undefined) => void,
|
|
14
|
+
) {
|
|
15
|
+
const container = new Container();
|
|
16
|
+
container.addChild(new Text(theme.fg("accent", theme.bold(title)), 1, 0));
|
|
17
|
+
|
|
18
|
+
const list = new SelectList(
|
|
19
|
+
options.map((option) => ({ value: option, label: option })),
|
|
20
|
+
Math.min(options.length, 12),
|
|
21
|
+
{
|
|
22
|
+
selectedPrefix: (text) => theme.fg("accent", text),
|
|
23
|
+
selectedText: (text) => theme.fg("accent", text),
|
|
24
|
+
description: (text) => theme.fg("muted", text),
|
|
25
|
+
scrollInfo: (text) => theme.fg("muted", text),
|
|
26
|
+
noMatch: (text) => theme.fg("warning", text),
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
truncatePrimary: ({ text, maxWidth, isSelected }) => {
|
|
30
|
+
const clipped = truncateToWidth(text, maxWidth, "");
|
|
31
|
+
// Keep the terminal's own foreground color so this remains readable even
|
|
32
|
+
// when Pi's configured theme does not match the terminal background.
|
|
33
|
+
return isSelected ? clipped : theme.bold(clipped);
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
list.onSelect = (item) => done(item.value);
|
|
38
|
+
list.onCancel = () => done(undefined);
|
|
39
|
+
container.addChild(list);
|
|
40
|
+
container.addChild(new Text(theme.fg("muted", "↑↓ navigate · Enter select · Esc cancel"), 1, 0));
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
render: (width: number) => container.render(width),
|
|
44
|
+
invalidate: () => container.invalidate(),
|
|
45
|
+
handleInput(data: string) {
|
|
46
|
+
list.handleInput(data);
|
|
47
|
+
tui.requestRender();
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function selectMenu(
|
|
53
|
+
ctx: ExtensionCommandContext,
|
|
54
|
+
title: string,
|
|
55
|
+
options: readonly string[],
|
|
56
|
+
): Promise<string | undefined> {
|
|
57
|
+
if (options.length === 0) return undefined;
|
|
58
|
+
if (!supportsThemedMenu(ctx)) return ctx.ui.select(title, [...options]);
|
|
59
|
+
return ctx.ui.custom<string | undefined>((tui, theme, _keybindings, done) =>
|
|
60
|
+
menuComponent(tui, theme, title, options, done));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function confirmMenu(
|
|
64
|
+
ctx: ExtensionCommandContext,
|
|
65
|
+
title: string,
|
|
66
|
+
message: string,
|
|
67
|
+
): Promise<boolean> {
|
|
68
|
+
if (!supportsThemedMenu(ctx)) return ctx.ui.confirm(title, message);
|
|
69
|
+
return await selectMenu(ctx, `${title}\n${message}`, ["Yes", "No"]) === "Yes";
|
|
70
|
+
}
|
package/src/session-state.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import type { Character } from "./character-loader.ts";
|
|
2
2
|
|
|
3
|
+
export type AvatarMode = "auto" | "full" | "compact" | "off";
|
|
4
|
+
|
|
3
5
|
export interface IncarnateStateSnapshot {
|
|
4
6
|
activeCharacter?: Character;
|
|
5
7
|
currentMood?: string;
|
|
8
|
+
avatarMode: AvatarMode;
|
|
6
9
|
avatarEnabled: boolean;
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
export class IncarnateSessionState {
|
|
10
13
|
#activeCharacter: Character | undefined;
|
|
11
14
|
#currentMood: string | undefined;
|
|
12
|
-
#
|
|
15
|
+
#avatarMode: AvatarMode = "auto";
|
|
13
16
|
|
|
14
17
|
get activeCharacter(): Character | undefined {
|
|
15
18
|
return this.#activeCharacter;
|
|
@@ -20,7 +23,11 @@ export class IncarnateSessionState {
|
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
get avatarEnabled(): boolean {
|
|
23
|
-
return this.#
|
|
26
|
+
return this.#avatarMode !== "off";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get avatarMode(): AvatarMode {
|
|
30
|
+
return this.#avatarMode;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
activate(character: Character): void {
|
|
@@ -41,19 +48,24 @@ export class IncarnateSessionState {
|
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
setAvatarEnabled(enabled: boolean): void {
|
|
44
|
-
this.#
|
|
51
|
+
this.#avatarMode = enabled ? "auto" : "off";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setAvatarMode(mode: AvatarMode): void {
|
|
55
|
+
this.#avatarMode = mode;
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
reset(): void {
|
|
48
59
|
this.deactivate();
|
|
49
|
-
this.#
|
|
60
|
+
this.#avatarMode = "auto";
|
|
50
61
|
}
|
|
51
62
|
|
|
52
63
|
snapshot(): IncarnateStateSnapshot {
|
|
53
64
|
return {
|
|
54
65
|
activeCharacter: this.#activeCharacter,
|
|
55
66
|
currentMood: this.#currentMood,
|
|
56
|
-
|
|
67
|
+
avatarMode: this.#avatarMode,
|
|
68
|
+
avatarEnabled: this.avatarEnabled,
|
|
57
69
|
};
|
|
58
70
|
}
|
|
59
71
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
import type { Avatar } from "./avatar.ts";
|
|
4
|
+
import { characterSource, type CharacterLocations, type CharacterSource } from "./character-catalog.ts";
|
|
5
|
+
import type { AvatarMode, IncarnateSessionState } from "./session-state.ts";
|
|
6
|
+
|
|
7
|
+
export const UI_REQUEST_STATE_EVENT = "pi-incarnate:ui:request-state-v1";
|
|
8
|
+
export const UI_STATE_EVENT = "pi-incarnate:ui:state-v1";
|
|
9
|
+
|
|
10
|
+
export interface IncarnateUiStateV1 {
|
|
11
|
+
version: 1;
|
|
12
|
+
active: boolean;
|
|
13
|
+
avatarMode: AvatarMode;
|
|
14
|
+
character?: {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
mood?: string;
|
|
18
|
+
source: CharacterSource;
|
|
19
|
+
forms: {
|
|
20
|
+
available: number;
|
|
21
|
+
total: number;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
avatar?: {
|
|
25
|
+
lines: string[];
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
truncated: boolean;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isUiProtocolV1(value: unknown): boolean {
|
|
33
|
+
return Boolean(value && typeof value === "object" && (value as { version?: unknown }).version === 1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createUiStateSnapshot(
|
|
37
|
+
state: IncarnateSessionState,
|
|
38
|
+
locations: CharacterLocations,
|
|
39
|
+
avatar: Avatar | undefined,
|
|
40
|
+
): IncarnateUiStateV1 {
|
|
41
|
+
const character = state.activeCharacter;
|
|
42
|
+
if (!character) return { version: 1, active: false, avatarMode: state.avatarMode };
|
|
43
|
+
|
|
44
|
+
const availableForms = character.forms.filter((form) => form.status === "available").length;
|
|
45
|
+
const snapshot: IncarnateUiStateV1 = {
|
|
46
|
+
version: 1,
|
|
47
|
+
active: true,
|
|
48
|
+
avatarMode: state.avatarMode,
|
|
49
|
+
character: {
|
|
50
|
+
id: character.id,
|
|
51
|
+
name: character.name,
|
|
52
|
+
mood: state.currentMood,
|
|
53
|
+
source: characterSource(character, locations),
|
|
54
|
+
forms: { available: availableForms, total: character.forms.length },
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
if (avatar && state.avatarEnabled) {
|
|
58
|
+
snapshot.avatar = {
|
|
59
|
+
lines: [...avatar.lines],
|
|
60
|
+
width: avatar.lines.reduce((maximum, line) => Math.max(maximum, visibleWidth(line)), 0),
|
|
61
|
+
height: avatar.lines.length,
|
|
62
|
+
truncated: avatar.truncated,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return snapshot;
|
|
66
|
+
}
|