@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.
@@ -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 server-root-relative path scopes persona events to this chat.
640
- const path = (_a = chatModel === null || chatModel === void 0 ? void 0 : chatModel.name) !== null && _a !== void 0 ? _a : null;
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 && path ? sessionRegistry.get(path) : null), [sessionRegistry, path]);
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 || !path || !chatModel) {
734
+ if (!sessionRegistry || !chatId || !chatModel) {
716
735
  return;
717
736
  }
718
- const onDisposed = () => sessionRegistry.discard(path);
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, path, chatModel]);
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.
@@ -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
- path?: string;
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 path: string;
37
- constructor(path: string);
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 path. Components call
69
- * this with their chat's `IChatModel.name`.
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(path: string): PersonaManagerSessionState;
72
- /** Whether a manager session state exists for `path` (without creating one). */
73
- has(path: string): boolean;
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(path: string): void;
78
+ discard(chatId: string): void;
79
79
  private _onPersonasEvent;
80
80
  private _onPersonaStateEvent;
81
- private _byPath;
81
+ private _byChatId;
82
82
  }
83
83
  /**
84
84
  * Plugin token for the shared `PersonaSessionRegistry`. Consumed by the toolbar
@@ -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(path) {
28
- this.path = path;
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.path) {
85
+ if (!data.chat_id) {
86
86
  return;
87
87
  }
88
- this.get(data.path).updatePersonas(Array.isArray(data.personas) ? data.personas : []);
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.path || !data.persona_id) {
92
+ if (!data.chat_id || !data.persona_id) {
93
93
  return;
94
94
  }
95
- this.get(data.path).updatePersonaState(data.persona_id, data);
95
+ this.get(data.chat_id).updatePersonaState(data.persona_id, data);
96
96
  };
97
- this._byPath = new Map();
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 path. Components call
103
- * this with their chat's `IChatModel.name`.
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(path) {
106
- let state = this._byPath.get(path);
105
+ get(chatId) {
106
+ let state = this._byChatId.get(chatId);
107
107
  if (!state) {
108
- state = new PersonaManagerSessionState(path);
109
- this._byPath.set(path, state);
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 `path` (without creating one). */
114
- has(path) {
115
- return this._byPath.has(path);
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(path) {
122
- const state = this._byPath.get(path);
121
+ discard(chatId) {
122
+ const state = this._byChatId.get(chatId);
123
123
  if (state) {
124
124
  state.dispose();
125
- this._byPath.delete(path);
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(chatPath: string): Promise<void>;
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(chatPath) {
41
+ export async function cancelResponse(chatId) {
42
42
  try {
43
- await requestAPI('api/ai', `personas/cancel?chat_path=${encodeURIComponent(chatPath)}`, { method: 'POST' });
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);
@@ -34,15 +34,15 @@ export class SlashCommandProvider {
34
34
  if (!currentWord.startsWith('/')) {
35
35
  return [];
36
36
  }
37
- const path = (_b = (_a = inputModel.chatContext) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : null;
38
- if (!path) {
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(path).getPersona(personaId);
45
+ const persona = this._registry.get(chatId).getPersona(personaId);
46
46
  if (!persona) {
47
47
  return [];
48
48
  }
@@ -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.name);
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-a2",
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.23.0-alpha.4",
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 path', async () => {
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
- path: 'a.chat',
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
- path: 'a.chat',
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
- path: 'a.chat',
77
+ chat_id: 'a.chat',
78
78
  personas: []
79
79
  });
80
80
  await events.emit(PERSONA_STATE_EVENT_SCHEMA_ID, {
81
- path: 'a.chat',
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
- path: 'a.chat',
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');
@@ -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 server-root-relative path scopes persona events to this chat.
1079
- const path = chatModel?.name ?? null;
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 && path ? sessionRegistry.get(path) : null),
1085
- [sessionRegistry, path]
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 || !path || !chatModel) {
1191
+ if (!sessionRegistry || !chatId || !chatModel) {
1173
1192
  return;
1174
1193
  }
1175
- const onDisposed = () => sessionRegistry.discard(path);
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, path, chatModel]);
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
@@ -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 `path`); the `PersonaSessionRegistry` routes them to the per-chat
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
- path?: string;
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
- path?: string;
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 path: string) {}
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 path. Components call
155
- * this with their chat's `IChatModel.name`.
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(path: string): PersonaManagerSessionState {
158
- let state = this._byPath.get(path);
157
+ get(chatId: string): PersonaManagerSessionState {
158
+ let state = this._byChatId.get(chatId);
159
159
  if (!state) {
160
- state = new PersonaManagerSessionState(path);
161
- this._byPath.set(path, state);
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 `path` (without creating one). */
167
- has(path: string): boolean {
168
- return this._byPath.has(path);
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(path: string): void {
176
- const state = this._byPath.get(path);
175
+ discard(chatId: string): void {
176
+ const state = this._byChatId.get(chatId);
177
177
  if (state) {
178
178
  state.dispose();
179
- this._byPath.delete(path);
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.path) {
189
+ if (!data.chat_id) {
190
190
  return;
191
191
  }
192
- this.get(data.path).updatePersonas(
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.path || !data.persona_id) {
203
+ if (!data.chat_id || !data.persona_id) {
204
204
  return;
205
205
  }
206
- this.get(data.path).updatePersonaState(data.persona_id, data);
206
+ this.get(data.chat_id).updatePersonaState(data.persona_id, data);
207
207
  };
208
208
 
209
- private _byPath = new Map<string, PersonaManagerSessionState>();
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(chatPath: string): Promise<void> {
51
+ export async function cancelResponse(chatId: string): Promise<void> {
52
52
  try {
53
53
  await requestAPI(
54
54
  'api/ai',
55
- `personas/cancel?chat_path=${encodeURIComponent(chatPath)}`,
55
+ `personas/cancel?chat_id=${encodeURIComponent(chatId)}`,
56
56
  { method: 'POST' }
57
57
  );
58
58
  } catch (e) {
@@ -43,8 +43,8 @@ export class SlashCommandProvider implements IChatCommandProvider {
43
43
  return [];
44
44
  }
45
45
 
46
- const path = inputModel.chatContext?.name ?? null;
47
- if (!path) {
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(path).getPersona(personaId);
56
+ const persona = this._registry.get(chatId).getPersona(personaId);
57
57
  if (!persona) {
58
58
  return [];
59
59
  }
@@ -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.name);
47
+ await cancelResponse(chatModel.id);
48
48
  } finally {
49
49
  setInFlight(false);
50
50
  }