@jupyter-ai/persona-manager 0.2.0-a2 → 0.2.0-a3
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/lib/persona-controls.js +25 -6
- package/lib/persona-events.d.ts +10 -10
- package/lib/persona-events.js +19 -19
- package/lib/request.d.ts +1 -1
- package/lib/request.js +2 -2
- package/lib/slash-commands.js +3 -3
- package/lib/stop-button.js +2 -2
- package/package.json +2 -2
- package/src/__tests__/persona-events.spec.ts +6 -6
- package/src/persona-controls.tsx +26 -7
- package/src/persona-events.ts +22 -22
- package/src/request.ts +2 -2
- package/src/slash-commands.ts +3 -3
- package/src/stop-button.tsx +2 -2
package/lib/persona-controls.js
CHANGED
|
@@ -636,11 +636,30 @@ export function UsageChip(props) {
|
|
|
636
636
|
export function PersonaControls(props) {
|
|
637
637
|
var _a, _b, _c, _d, _e, _f;
|
|
638
638
|
const { chatModel, model, controlRegistry, sessionRegistry } = props;
|
|
639
|
-
// The chat's
|
|
640
|
-
|
|
639
|
+
// The chat's stable id scopes persona events to this chat. It is assigned
|
|
640
|
+
// asynchronously (once the model is `ready`: the WS connection frame arrives,
|
|
641
|
+
// or the RTC document syncs), so track it in state and update it when the
|
|
642
|
+
// model becomes ready. Reading it synchronously would capture `undefined`
|
|
643
|
+
// forever, since `id` is a plain accessor with no change signal.
|
|
644
|
+
const [chatId, setChatId] = useState((_a = chatModel === null || chatModel === void 0 ? void 0 : chatModel.id) !== null && _a !== void 0 ? _a : null);
|
|
645
|
+
useEffect(() => {
|
|
646
|
+
if (!chatModel) {
|
|
647
|
+
setChatId(null);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
let cancelled = false;
|
|
651
|
+
void chatModel.ready.then(id => {
|
|
652
|
+
if (!cancelled) {
|
|
653
|
+
setChatId(id !== null && id !== void 0 ? id : null);
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
return () => {
|
|
657
|
+
cancelled = true;
|
|
658
|
+
};
|
|
659
|
+
}, [chatModel]);
|
|
641
660
|
// The per-chat persona session state, built from persona events and shared
|
|
642
661
|
// via the registry. Created on demand; discarded when the chat closes.
|
|
643
|
-
const managerState = useMemo(() => (sessionRegistry &&
|
|
662
|
+
const managerState = useMemo(() => (sessionRegistry && chatId ? sessionRegistry.get(chatId) : null), [sessionRegistry, chatId]);
|
|
644
663
|
const [personas, setPersonas] = useState([]);
|
|
645
664
|
// Whether a persona list has been received for this chat yet. Before that, an
|
|
646
665
|
// empty list means "still loading", not "this chat has no personas".
|
|
@@ -712,15 +731,15 @@ export function PersonaControls(props) {
|
|
|
712
731
|
// Discard this chat's session state when the chat model is disposed (chat
|
|
713
732
|
// closed), freeing its memory.
|
|
714
733
|
useEffect(() => {
|
|
715
|
-
if (!sessionRegistry || !
|
|
734
|
+
if (!sessionRegistry || !chatId || !chatModel) {
|
|
716
735
|
return;
|
|
717
736
|
}
|
|
718
|
-
const onDisposed = () => sessionRegistry.discard(
|
|
737
|
+
const onDisposed = () => sessionRegistry.discard(chatId);
|
|
719
738
|
chatModel.disposed.connect(onDisposed);
|
|
720
739
|
return () => {
|
|
721
740
|
chatModel.disposed.disconnect(onDisposed);
|
|
722
741
|
};
|
|
723
|
-
}, [sessionRegistry,
|
|
742
|
+
}, [sessionRegistry, chatId, chatModel]);
|
|
724
743
|
// Stamp the current persona + its settings onto the input model's metadata,
|
|
725
744
|
// so it rides out with the next message and the PersonaManager routes and
|
|
726
745
|
// applies it. Keyed on a signature so we only write when it changes.
|
package/lib/persona-events.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export declare const PERSONAS_EVENT_SCHEMA_ID = "https://schema.jupyter.org/jupy
|
|
|
7
7
|
export declare const PERSONA_STATE_EVENT_SCHEMA_ID = "https://schema.jupyter.org/jupyter_ai_persona_manager/persona_state/v1";
|
|
8
8
|
/** The wire shape of a `persona_state` event. */
|
|
9
9
|
type PersonaStatePayload = {
|
|
10
|
-
|
|
10
|
+
chat_id?: string;
|
|
11
11
|
persona_id?: string;
|
|
12
12
|
model?: ModelConfiguration;
|
|
13
13
|
settings?: SettingConfiguration[];
|
|
@@ -33,8 +33,8 @@ export declare class PersonaSessionState {
|
|
|
33
33
|
* or any persona's state updates, so React components re-render.
|
|
34
34
|
*/
|
|
35
35
|
export declare class PersonaManagerSessionState implements IDisposable {
|
|
36
|
-
readonly
|
|
37
|
-
constructor(
|
|
36
|
+
readonly chatId: string;
|
|
37
|
+
constructor(chatId: string);
|
|
38
38
|
/** Emits whenever the persona list or a persona's state changes. */
|
|
39
39
|
get changed(): ISignal<this, void>;
|
|
40
40
|
/** Whether a `personas` event has been received for this chat yet. */
|
|
@@ -65,20 +65,20 @@ export declare class PersonaManagerSessionState implements IDisposable {
|
|
|
65
65
|
export declare class PersonaSessionRegistry {
|
|
66
66
|
constructor(eventListener: IEventListener);
|
|
67
67
|
/**
|
|
68
|
-
* Get (or create) the manager session state for a chat
|
|
69
|
-
*
|
|
68
|
+
* Get (or create) the manager session state for a chat. Components call this
|
|
69
|
+
* with their chat's stable id (`IChatModel.id` / `IChatContext.id`).
|
|
70
70
|
*/
|
|
71
|
-
get(
|
|
72
|
-
/** Whether a manager session state exists for `
|
|
73
|
-
has(
|
|
71
|
+
get(chatId: string): PersonaManagerSessionState;
|
|
72
|
+
/** Whether a manager session state exists for `chatId` (without creating one). */
|
|
73
|
+
has(chatId: string): boolean;
|
|
74
74
|
/**
|
|
75
75
|
* Discard a chat's session state and free its memory. Called when the client
|
|
76
76
|
* closes the chat (wired to the chat model's `disposed` signal).
|
|
77
77
|
*/
|
|
78
|
-
discard(
|
|
78
|
+
discard(chatId: string): void;
|
|
79
79
|
private _onPersonasEvent;
|
|
80
80
|
private _onPersonaStateEvent;
|
|
81
|
-
private
|
|
81
|
+
private _byChatId;
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
84
|
* Plugin token for the shared `PersonaSessionRegistry`. Consumed by the toolbar
|
package/lib/persona-events.js
CHANGED
|
@@ -24,8 +24,8 @@ export class PersonaSessionState {
|
|
|
24
24
|
* or any persona's state updates, so React components re-render.
|
|
25
25
|
*/
|
|
26
26
|
export class PersonaManagerSessionState {
|
|
27
|
-
constructor(
|
|
28
|
-
this.
|
|
27
|
+
constructor(chatId) {
|
|
28
|
+
this.chatId = chatId;
|
|
29
29
|
this._personasReceived = false;
|
|
30
30
|
this._personas = [];
|
|
31
31
|
this._states = new Map();
|
|
@@ -82,47 +82,47 @@ export class PersonaSessionRegistry {
|
|
|
82
82
|
constructor(eventListener) {
|
|
83
83
|
this._onPersonasEvent = async (_manager, _schemaId, event) => {
|
|
84
84
|
const data = event;
|
|
85
|
-
if (!data.
|
|
85
|
+
if (!data.chat_id) {
|
|
86
86
|
return;
|
|
87
87
|
}
|
|
88
|
-
this.get(data.
|
|
88
|
+
this.get(data.chat_id).updatePersonas(Array.isArray(data.personas) ? data.personas : []);
|
|
89
89
|
};
|
|
90
90
|
this._onPersonaStateEvent = async (_manager, _schemaId, event) => {
|
|
91
91
|
const data = event;
|
|
92
|
-
if (!data.
|
|
92
|
+
if (!data.chat_id || !data.persona_id) {
|
|
93
93
|
return;
|
|
94
94
|
}
|
|
95
|
-
this.get(data.
|
|
95
|
+
this.get(data.chat_id).updatePersonaState(data.persona_id, data);
|
|
96
96
|
};
|
|
97
|
-
this.
|
|
97
|
+
this._byChatId = new Map();
|
|
98
98
|
eventListener.addListener(PERSONAS_EVENT_SCHEMA_ID, this._onPersonasEvent);
|
|
99
99
|
eventListener.addListener(PERSONA_STATE_EVENT_SCHEMA_ID, this._onPersonaStateEvent);
|
|
100
100
|
}
|
|
101
101
|
/**
|
|
102
|
-
* Get (or create) the manager session state for a chat
|
|
103
|
-
*
|
|
102
|
+
* Get (or create) the manager session state for a chat. Components call this
|
|
103
|
+
* with their chat's stable id (`IChatModel.id` / `IChatContext.id`).
|
|
104
104
|
*/
|
|
105
|
-
get(
|
|
106
|
-
let state = this.
|
|
105
|
+
get(chatId) {
|
|
106
|
+
let state = this._byChatId.get(chatId);
|
|
107
107
|
if (!state) {
|
|
108
|
-
state = new PersonaManagerSessionState(
|
|
109
|
-
this.
|
|
108
|
+
state = new PersonaManagerSessionState(chatId);
|
|
109
|
+
this._byChatId.set(chatId, state);
|
|
110
110
|
}
|
|
111
111
|
return state;
|
|
112
112
|
}
|
|
113
|
-
/** Whether a manager session state exists for `
|
|
114
|
-
has(
|
|
115
|
-
return this.
|
|
113
|
+
/** Whether a manager session state exists for `chatId` (without creating one). */
|
|
114
|
+
has(chatId) {
|
|
115
|
+
return this._byChatId.has(chatId);
|
|
116
116
|
}
|
|
117
117
|
/**
|
|
118
118
|
* Discard a chat's session state and free its memory. Called when the client
|
|
119
119
|
* closes the chat (wired to the chat model's `disposed` signal).
|
|
120
120
|
*/
|
|
121
|
-
discard(
|
|
122
|
-
const state = this.
|
|
121
|
+
discard(chatId) {
|
|
122
|
+
const state = this._byChatId.get(chatId);
|
|
123
123
|
if (state) {
|
|
124
124
|
state.dispose();
|
|
125
|
-
this.
|
|
125
|
+
this._byChatId.delete(chatId);
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
}
|
package/lib/request.d.ts
CHANGED
|
@@ -12,4 +12,4 @@ export declare function requestAPI<T>(namespace: string, endPoint?: string, init
|
|
|
12
12
|
* via the cancel endpoint. Backend-agnostic: each persona's
|
|
13
13
|
* `cancel_response()` decides what stopping means.
|
|
14
14
|
*/
|
|
15
|
-
export declare function cancelResponse(
|
|
15
|
+
export declare function cancelResponse(chatId: string): Promise<void>;
|
package/lib/request.js
CHANGED
|
@@ -38,9 +38,9 @@ export async function requestAPI(namespace, endPoint = '', init = {}) {
|
|
|
38
38
|
* via the cancel endpoint. Backend-agnostic: each persona's
|
|
39
39
|
* `cancel_response()` decides what stopping means.
|
|
40
40
|
*/
|
|
41
|
-
export async function cancelResponse(
|
|
41
|
+
export async function cancelResponse(chatId) {
|
|
42
42
|
try {
|
|
43
|
-
await requestAPI('api/ai', `personas/cancel?
|
|
43
|
+
await requestAPI('api/ai', `personas/cancel?chat_id=${encodeURIComponent(chatId)}`, { method: 'POST' });
|
|
44
44
|
}
|
|
45
45
|
catch (e) {
|
|
46
46
|
console.warn('Error cancelling response: ', e);
|
package/lib/slash-commands.js
CHANGED
|
@@ -34,15 +34,15 @@ export class SlashCommandProvider {
|
|
|
34
34
|
if (!currentWord.startsWith('/')) {
|
|
35
35
|
return [];
|
|
36
36
|
}
|
|
37
|
-
const
|
|
38
|
-
if (!
|
|
37
|
+
const chatId = (_b = (_a = inputModel.chatContext) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null;
|
|
38
|
+
if (!chatId) {
|
|
39
39
|
return [];
|
|
40
40
|
}
|
|
41
41
|
const personaId = (_d = (_c = inputModel.getMetadata) === null || _c === void 0 ? void 0 : _c.call(inputModel).to_persona) !== null && _d !== void 0 ? _d : null;
|
|
42
42
|
if (!personaId) {
|
|
43
43
|
return [];
|
|
44
44
|
}
|
|
45
|
-
const persona = this._registry.get(
|
|
45
|
+
const persona = this._registry.get(chatId).getPersona(personaId);
|
|
46
46
|
if (!persona) {
|
|
47
47
|
return [];
|
|
48
48
|
}
|
package/lib/stop-button.js
CHANGED
|
@@ -31,12 +31,12 @@ export function StopButton(props) {
|
|
|
31
31
|
};
|
|
32
32
|
}, [chatModel]);
|
|
33
33
|
async function handleStop() {
|
|
34
|
-
if (!chatModel) {
|
|
34
|
+
if (!(chatModel === null || chatModel === void 0 ? void 0 : chatModel.id)) {
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
37
|
setInFlight(true);
|
|
38
38
|
try {
|
|
39
|
-
await cancelResponse(chatModel.
|
|
39
|
+
await cancelResponse(chatModel.id);
|
|
40
40
|
}
|
|
41
41
|
finally {
|
|
42
42
|
setInFlight(false);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupyter-ai/persona-manager",
|
|
3
|
-
"version": "0.2.0-
|
|
3
|
+
"version": "0.2.0-a3",
|
|
4
4
|
"description": "The core manager & registry for AI personas in Jupyter AI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"watch:labextension": "jupyter labextension watch ."
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@jupyter/chat": "^0.
|
|
61
|
+
"@jupyter/chat": "^0.25.0-alpha.5",
|
|
62
62
|
"@jupyterlab/application": "^4.0.0",
|
|
63
63
|
"@jupyterlab/coreutils": "^6.0.0",
|
|
64
64
|
"@jupyterlab/services": "^7.0.0",
|
|
@@ -41,10 +41,10 @@ function makeRegistry(): {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
describe('PersonaSessionRegistry', () => {
|
|
44
|
-
it('routes a personas event to the matching chat by
|
|
44
|
+
it('routes a personas event to the matching chat by id', async () => {
|
|
45
45
|
const { registry, events } = makeRegistry();
|
|
46
46
|
await events.emit(PERSONAS_EVENT_SCHEMA_ID, {
|
|
47
|
-
|
|
47
|
+
chat_id: 'a.chat',
|
|
48
48
|
personas: [{ id: 'p1', name: 'One', avatar_url: null }]
|
|
49
49
|
});
|
|
50
50
|
expect(registry.get('a.chat').personas.map(p => p.id)).toEqual(['p1']);
|
|
@@ -56,7 +56,7 @@ describe('PersonaSessionRegistry', () => {
|
|
|
56
56
|
it('routes persona_state events and builds PersonaSessionState', async () => {
|
|
57
57
|
const { registry, events } = makeRegistry();
|
|
58
58
|
await events.emit(PERSONA_STATE_EVENT_SCHEMA_ID, {
|
|
59
|
-
|
|
59
|
+
chat_id: 'a.chat',
|
|
60
60
|
persona_id: 'p1',
|
|
61
61
|
model: { current: 'm1', options: [], settings: [] },
|
|
62
62
|
usage: { input_tokens: 5 }
|
|
@@ -74,11 +74,11 @@ describe('PersonaSessionRegistry', () => {
|
|
|
74
74
|
fired += 1;
|
|
75
75
|
});
|
|
76
76
|
await events.emit(PERSONAS_EVENT_SCHEMA_ID, {
|
|
77
|
-
|
|
77
|
+
chat_id: 'a.chat',
|
|
78
78
|
personas: []
|
|
79
79
|
});
|
|
80
80
|
await events.emit(PERSONA_STATE_EVENT_SCHEMA_ID, {
|
|
81
|
-
|
|
81
|
+
chat_id: 'a.chat',
|
|
82
82
|
persona_id: 'p1'
|
|
83
83
|
});
|
|
84
84
|
expect(fired).toBe(2);
|
|
@@ -87,7 +87,7 @@ describe('PersonaSessionRegistry', () => {
|
|
|
87
87
|
it('discards a chat session state on close, freeing memory', async () => {
|
|
88
88
|
const { registry, events } = makeRegistry();
|
|
89
89
|
await events.emit(PERSONAS_EVENT_SCHEMA_ID, {
|
|
90
|
-
|
|
90
|
+
chat_id: 'a.chat',
|
|
91
91
|
personas: [{ id: 'p1', name: 'One', avatar_url: null }]
|
|
92
92
|
});
|
|
93
93
|
const state = registry.get('a.chat');
|
package/src/persona-controls.tsx
CHANGED
|
@@ -1075,14 +1075,33 @@ export function PersonaControls(
|
|
|
1075
1075
|
}
|
|
1076
1076
|
): JSX.Element | null {
|
|
1077
1077
|
const { chatModel, model, controlRegistry, sessionRegistry } = props;
|
|
1078
|
-
// The chat's
|
|
1079
|
-
|
|
1078
|
+
// The chat's stable id scopes persona events to this chat. It is assigned
|
|
1079
|
+
// asynchronously (once the model is `ready`: the WS connection frame arrives,
|
|
1080
|
+
// or the RTC document syncs), so track it in state and update it when the
|
|
1081
|
+
// model becomes ready. Reading it synchronously would capture `undefined`
|
|
1082
|
+
// forever, since `id` is a plain accessor with no change signal.
|
|
1083
|
+
const [chatId, setChatId] = useState<string | null>(chatModel?.id ?? null);
|
|
1084
|
+
useEffect(() => {
|
|
1085
|
+
if (!chatModel) {
|
|
1086
|
+
setChatId(null);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
let cancelled = false;
|
|
1090
|
+
void chatModel.ready.then(id => {
|
|
1091
|
+
if (!cancelled) {
|
|
1092
|
+
setChatId(id ?? null);
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
1095
|
+
return () => {
|
|
1096
|
+
cancelled = true;
|
|
1097
|
+
};
|
|
1098
|
+
}, [chatModel]);
|
|
1080
1099
|
|
|
1081
1100
|
// The per-chat persona session state, built from persona events and shared
|
|
1082
1101
|
// via the registry. Created on demand; discarded when the chat closes.
|
|
1083
1102
|
const managerState = useMemo(
|
|
1084
|
-
() => (sessionRegistry &&
|
|
1085
|
-
[sessionRegistry,
|
|
1103
|
+
() => (sessionRegistry && chatId ? sessionRegistry.get(chatId) : null),
|
|
1104
|
+
[sessionRegistry, chatId]
|
|
1086
1105
|
);
|
|
1087
1106
|
|
|
1088
1107
|
const [personas, setPersonas] = useState<PersonaOption[]>([]);
|
|
@@ -1169,15 +1188,15 @@ export function PersonaControls(
|
|
|
1169
1188
|
// Discard this chat's session state when the chat model is disposed (chat
|
|
1170
1189
|
// closed), freeing its memory.
|
|
1171
1190
|
useEffect(() => {
|
|
1172
|
-
if (!sessionRegistry || !
|
|
1191
|
+
if (!sessionRegistry || !chatId || !chatModel) {
|
|
1173
1192
|
return;
|
|
1174
1193
|
}
|
|
1175
|
-
const onDisposed = () => sessionRegistry.discard(
|
|
1194
|
+
const onDisposed = () => sessionRegistry.discard(chatId);
|
|
1176
1195
|
chatModel.disposed.connect(onDisposed);
|
|
1177
1196
|
return () => {
|
|
1178
1197
|
chatModel.disposed.disconnect(onDisposed);
|
|
1179
1198
|
};
|
|
1180
|
-
}, [sessionRegistry,
|
|
1199
|
+
}, [sessionRegistry, chatId, chatModel]);
|
|
1181
1200
|
|
|
1182
1201
|
// Stamp the current persona + its settings onto the input model's metadata,
|
|
1183
1202
|
// so it rides out with the next message and the PersonaManager routes and
|
package/src/persona-events.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* `jupyterlab-eventlistener`), replacing the previous Yjs-awareness channel.
|
|
4
4
|
*
|
|
5
5
|
* Flow: the server emits `personas` / `persona_state` events (each carrying the
|
|
6
|
-
* chat's `
|
|
7
|
-
* `PersonaManagerSessionState`, which holds the persona list and a
|
|
6
|
+
* chat's stable `chat_id`); the `PersonaSessionRegistry` routes them to the
|
|
7
|
+
* per-chat `PersonaManagerSessionState`, which holds the persona list and a
|
|
8
8
|
* `PersonaSessionState` per persona and fires a Lumino `changed` signal. React
|
|
9
9
|
* components listen to that signal. When a chat closes, its state is discarded.
|
|
10
10
|
*
|
|
@@ -34,7 +34,7 @@ export const PERSONA_STATE_EVENT_SCHEMA_ID =
|
|
|
34
34
|
|
|
35
35
|
/** The wire shape of a `persona_state` event. */
|
|
36
36
|
type PersonaStatePayload = {
|
|
37
|
-
|
|
37
|
+
chat_id?: string;
|
|
38
38
|
persona_id?: string;
|
|
39
39
|
model?: ModelConfiguration;
|
|
40
40
|
settings?: SettingConfiguration[];
|
|
@@ -44,7 +44,7 @@ type PersonaStatePayload = {
|
|
|
44
44
|
|
|
45
45
|
/** The wire shape of a `personas` event. */
|
|
46
46
|
type PersonasPayload = {
|
|
47
|
-
|
|
47
|
+
chat_id?: string;
|
|
48
48
|
personas?: PersonaOption[];
|
|
49
49
|
};
|
|
50
50
|
|
|
@@ -76,7 +76,7 @@ export class PersonaSessionState {
|
|
|
76
76
|
* or any persona's state updates, so React components re-render.
|
|
77
77
|
*/
|
|
78
78
|
export class PersonaManagerSessionState implements IDisposable {
|
|
79
|
-
constructor(public readonly
|
|
79
|
+
constructor(public readonly chatId: string) {}
|
|
80
80
|
|
|
81
81
|
/** Emits whenever the persona list or a persona's state changes. */
|
|
82
82
|
get changed(): ISignal<this, void> {
|
|
@@ -151,32 +151,32 @@ export class PersonaSessionRegistry {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
/**
|
|
154
|
-
* Get (or create) the manager session state for a chat
|
|
155
|
-
*
|
|
154
|
+
* Get (or create) the manager session state for a chat. Components call this
|
|
155
|
+
* with their chat's stable id (`IChatModel.id` / `IChatContext.id`).
|
|
156
156
|
*/
|
|
157
|
-
get(
|
|
158
|
-
let state = this.
|
|
157
|
+
get(chatId: string): PersonaManagerSessionState {
|
|
158
|
+
let state = this._byChatId.get(chatId);
|
|
159
159
|
if (!state) {
|
|
160
|
-
state = new PersonaManagerSessionState(
|
|
161
|
-
this.
|
|
160
|
+
state = new PersonaManagerSessionState(chatId);
|
|
161
|
+
this._byChatId.set(chatId, state);
|
|
162
162
|
}
|
|
163
163
|
return state;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
/** Whether a manager session state exists for `
|
|
167
|
-
has(
|
|
168
|
-
return this.
|
|
166
|
+
/** Whether a manager session state exists for `chatId` (without creating one). */
|
|
167
|
+
has(chatId: string): boolean {
|
|
168
|
+
return this._byChatId.has(chatId);
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
/**
|
|
172
172
|
* Discard a chat's session state and free its memory. Called when the client
|
|
173
173
|
* closes the chat (wired to the chat model's `disposed` signal).
|
|
174
174
|
*/
|
|
175
|
-
discard(
|
|
176
|
-
const state = this.
|
|
175
|
+
discard(chatId: string): void {
|
|
176
|
+
const state = this._byChatId.get(chatId);
|
|
177
177
|
if (state) {
|
|
178
178
|
state.dispose();
|
|
179
|
-
this.
|
|
179
|
+
this._byChatId.delete(chatId);
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
|
|
@@ -186,10 +186,10 @@ export class PersonaSessionRegistry {
|
|
|
186
186
|
event: Event.Emission
|
|
187
187
|
): Promise<void> => {
|
|
188
188
|
const data = event as PersonasPayload;
|
|
189
|
-
if (!data.
|
|
189
|
+
if (!data.chat_id) {
|
|
190
190
|
return;
|
|
191
191
|
}
|
|
192
|
-
this.get(data.
|
|
192
|
+
this.get(data.chat_id).updatePersonas(
|
|
193
193
|
Array.isArray(data.personas) ? data.personas : []
|
|
194
194
|
);
|
|
195
195
|
};
|
|
@@ -200,13 +200,13 @@ export class PersonaSessionRegistry {
|
|
|
200
200
|
event: Event.Emission
|
|
201
201
|
): Promise<void> => {
|
|
202
202
|
const data = event as PersonaStatePayload;
|
|
203
|
-
if (!data.
|
|
203
|
+
if (!data.chat_id || !data.persona_id) {
|
|
204
204
|
return;
|
|
205
205
|
}
|
|
206
|
-
this.get(data.
|
|
206
|
+
this.get(data.chat_id).updatePersonaState(data.persona_id, data);
|
|
207
207
|
};
|
|
208
208
|
|
|
209
|
-
private
|
|
209
|
+
private _byChatId = new Map<string, PersonaManagerSessionState>();
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
/**
|
package/src/request.ts
CHANGED
|
@@ -48,11 +48,11 @@ export async function requestAPI<T>(
|
|
|
48
48
|
* via the cancel endpoint. Backend-agnostic: each persona's
|
|
49
49
|
* `cancel_response()` decides what stopping means.
|
|
50
50
|
*/
|
|
51
|
-
export async function cancelResponse(
|
|
51
|
+
export async function cancelResponse(chatId: string): Promise<void> {
|
|
52
52
|
try {
|
|
53
53
|
await requestAPI(
|
|
54
54
|
'api/ai',
|
|
55
|
-
`personas/cancel?
|
|
55
|
+
`personas/cancel?chat_id=${encodeURIComponent(chatId)}`,
|
|
56
56
|
{ method: 'POST' }
|
|
57
57
|
);
|
|
58
58
|
} catch (e) {
|
package/src/slash-commands.ts
CHANGED
|
@@ -43,8 +43,8 @@ export class SlashCommandProvider implements IChatCommandProvider {
|
|
|
43
43
|
return [];
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
const
|
|
47
|
-
if (!
|
|
46
|
+
const chatId = inputModel.chatContext?.id ?? null;
|
|
47
|
+
if (!chatId) {
|
|
48
48
|
return [];
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -53,7 +53,7 @@ export class SlashCommandProvider implements IChatCommandProvider {
|
|
|
53
53
|
return [];
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
const persona = this._registry.get(
|
|
56
|
+
const persona = this._registry.get(chatId).getPersona(personaId);
|
|
57
57
|
if (!persona) {
|
|
58
58
|
return [];
|
|
59
59
|
}
|
package/src/stop-button.tsx
CHANGED
|
@@ -38,13 +38,13 @@ export function StopButton(
|
|
|
38
38
|
}, [chatModel]);
|
|
39
39
|
|
|
40
40
|
async function handleStop() {
|
|
41
|
-
if (!chatModel) {
|
|
41
|
+
if (!chatModel?.id) {
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
setInFlight(true);
|
|
46
46
|
try {
|
|
47
|
-
await cancelResponse(chatModel.
|
|
47
|
+
await cancelResponse(chatModel.id);
|
|
48
48
|
} finally {
|
|
49
49
|
setInFlight(false);
|
|
50
50
|
}
|