@jupyter-ai/persona-manager 0.1.3 → 0.2.0-a1
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/awareness.d.ts +8 -77
- package/lib/awareness.js +10 -130
- package/lib/index.d.ts +3 -1
- package/lib/index.js +29 -12
- package/lib/persona-controls.d.ts +9 -3
- package/lib/persona-controls.js +57 -78
- package/lib/persona-events.d.ts +88 -0
- package/lib/persona-events.js +133 -0
- package/lib/slash-commands.d.ts +7 -3
- package/lib/slash-commands.js +10 -20
- package/package.json +3 -3
- package/src/__tests__/awareness-fixture.ts +14 -32
- package/src/__tests__/persona-events.spec.ts +106 -0
- package/src/awareness.ts +12 -146
- package/src/index.ts +44 -12
- package/src/persona-controls.tsx +72 -93
- package/src/persona-events.ts +218 -0
- package/src/slash-commands.ts +11 -18
- package/src/__tests__/awareness.spec.ts +0 -178
package/lib/awareness.d.ts
CHANGED
|
@@ -1,26 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Shared persona session data types.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* publishes under a fixed, hardcoded client ID.
|
|
9
|
-
* - `PersonaAwareness` reads one persona's slot (model configuration, settings,
|
|
10
|
-
* usage, slash commands, writing status), keyed by that persona's Yjs client
|
|
11
|
-
* ID (reported in the persona list).
|
|
12
|
-
*
|
|
13
|
-
* These mirror the Python awareness helpers of the same names. They are
|
|
14
|
-
* read-only: user selections ride on message metadata, not awareness.
|
|
15
|
-
*/
|
|
16
|
-
import { IAwareness } from '@jupyter/ydoc';
|
|
17
|
-
/**
|
|
18
|
-
* The fixed Yjs client ID the `PersonaManager` publishes its persona list under.
|
|
19
|
-
* A chosen 53-bit constant, hardcoded on both server and client so the browser
|
|
20
|
-
* can find the manager's slot without a discovery request. Must match
|
|
21
|
-
* `PERSONA_MANAGER_AWARENESS_CLIENT_ID` in the persona-manager Python package.
|
|
4
|
+
* These mirror the Pydantic models in the persona-manager Python package and are
|
|
5
|
+
* the payloads carried by the `personas` / `persona_state` Jupyter Events. The
|
|
6
|
+
* live, per-chat state built from those events lives in
|
|
7
|
+
* `PersonaManagerSessionState` / `PersonaSessionState` (see `./persona-events`).
|
|
22
8
|
*/
|
|
23
|
-
export declare const PERSONA_MANAGER_AWARENESS_CLIENT_ID = 7133713371337;
|
|
24
9
|
/** A selectable model. Mirrors `ModelOption` in the Python package. */
|
|
25
10
|
export type ModelOption = {
|
|
26
11
|
id: string;
|
|
@@ -91,62 +76,8 @@ export type PersonaOption = {
|
|
|
91
76
|
id: string;
|
|
92
77
|
name: string;
|
|
93
78
|
avatar_url: string | null;
|
|
94
|
-
/**
|
|
95
|
-
yjs_client_id
|
|
79
|
+
/** Deprecated: legacy Yjs client id, unused now that state rides events. */
|
|
80
|
+
yjs_client_id?: number;
|
|
96
81
|
};
|
|
97
82
|
export declare const EMPTY_USAGE: Usage;
|
|
98
|
-
|
|
99
|
-
* A typed, read-only view of the `PersonaManager`'s awareness slot: the list of
|
|
100
|
-
* personas in the chat. Construct it with `PersonaManagerAwareness.from()`,
|
|
101
|
-
* which resolves once the manager has published its slot.
|
|
102
|
-
*/
|
|
103
|
-
export declare class PersonaManagerAwareness {
|
|
104
|
-
private _awareness;
|
|
105
|
-
private constructor();
|
|
106
|
-
/**
|
|
107
|
-
* Resolve once the `PersonaManager` has published its slot under the fixed
|
|
108
|
-
* client ID, polling the awareness map until it appears (or rejecting after a
|
|
109
|
-
* bounded wait). Pass the chat's Yjs awareness object (e.g. from the chat
|
|
110
|
-
* model's shared model).
|
|
111
|
-
*
|
|
112
|
-
* Safe to `await` in any context: if the slot is already present it resolves
|
|
113
|
-
* on the first check, effectively immediately.
|
|
114
|
-
*/
|
|
115
|
-
static from(awareness: IAwareness): Promise<PersonaManagerAwareness>;
|
|
116
|
-
/**
|
|
117
|
-
* The personas available in this chat. Empty if none are published. Pass an
|
|
118
|
-
* entry to `PersonaAwareness.from()` to read that persona's own slot.
|
|
119
|
-
*/
|
|
120
|
-
get personas(): PersonaOption[];
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* A typed, read-only view of one persona's awareness slot. Construct it with
|
|
124
|
-
* `PersonaAwareness.from(awareness, personaOption)` — the `PersonaOption` comes
|
|
125
|
-
* from `PersonaManagerAwareness.personas` and carries the persona's client ID.
|
|
126
|
-
*/
|
|
127
|
-
export declare class PersonaAwareness {
|
|
128
|
-
private _awareness;
|
|
129
|
-
private _id;
|
|
130
|
-
private _clientId;
|
|
131
|
-
private constructor();
|
|
132
|
-
/** Build a view of the persona named by `option` (from the persona list). */
|
|
133
|
-
static from(awareness: IAwareness, option: PersonaOption): PersonaAwareness;
|
|
134
|
-
/** The persona's stable ID (from the persona list). */
|
|
135
|
-
get id(): string;
|
|
136
|
-
private get _state();
|
|
137
|
-
/** The persona's model configuration (current model, options, model settings). */
|
|
138
|
-
get model(): ModelConfiguration;
|
|
139
|
-
/** The persona's general (non-model) setting configurations. */
|
|
140
|
-
get settings(): SettingConfiguration[];
|
|
141
|
-
/** The token and cost usage the persona reports for the session. */
|
|
142
|
-
get usage(): Usage;
|
|
143
|
-
/** The slash commands the persona advertises. */
|
|
144
|
-
get slash_commands(): CommandOption[];
|
|
145
|
-
/**
|
|
146
|
-
* Whether the persona is currently writing: `false` when idle, or the ID of
|
|
147
|
-
* the message being written while streaming.
|
|
148
|
-
*/
|
|
149
|
-
get isWriting(): boolean | string;
|
|
150
|
-
/** Whether this persona has published its state yet. */
|
|
151
|
-
get isReady(): boolean;
|
|
152
|
-
}
|
|
83
|
+
export declare const EMPTY_MODEL_CONFIGURATION: ModelConfiguration;
|
package/lib/awareness.js
CHANGED
|
@@ -1,25 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Shared persona session data types.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* publishes under a fixed, hardcoded client ID.
|
|
9
|
-
* - `PersonaAwareness` reads one persona's slot (model configuration, settings,
|
|
10
|
-
* usage, slash commands, writing status), keyed by that persona's Yjs client
|
|
11
|
-
* ID (reported in the persona list).
|
|
12
|
-
*
|
|
13
|
-
* These mirror the Python awareness helpers of the same names. They are
|
|
14
|
-
* read-only: user selections ride on message metadata, not awareness.
|
|
4
|
+
* These mirror the Pydantic models in the persona-manager Python package and are
|
|
5
|
+
* the payloads carried by the `personas` / `persona_state` Jupyter Events. The
|
|
6
|
+
* live, per-chat state built from those events lives in
|
|
7
|
+
* `PersonaManagerSessionState` / `PersonaSessionState` (see `./persona-events`).
|
|
15
8
|
*/
|
|
16
|
-
/**
|
|
17
|
-
* The fixed Yjs client ID the `PersonaManager` publishes its persona list under.
|
|
18
|
-
* A chosen 53-bit constant, hardcoded on both server and client so the browser
|
|
19
|
-
* can find the manager's slot without a discovery request. Must match
|
|
20
|
-
* `PERSONA_MANAGER_AWARENESS_CLIENT_ID` in the persona-manager Python package.
|
|
21
|
-
*/
|
|
22
|
-
export const PERSONA_MANAGER_AWARENESS_CLIENT_ID = 7133713371337;
|
|
23
9
|
export const EMPTY_USAGE = {
|
|
24
10
|
context_tokens: null,
|
|
25
11
|
context_size: null,
|
|
@@ -33,114 +19,8 @@ export const EMPTY_USAGE = {
|
|
|
33
19
|
cost_amount: null,
|
|
34
20
|
cost_currency: null
|
|
35
21
|
};
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
42
|
-
/**
|
|
43
|
-
* A typed, read-only view of the `PersonaManager`'s awareness slot: the list of
|
|
44
|
-
* personas in the chat. Construct it with `PersonaManagerAwareness.from()`,
|
|
45
|
-
* which resolves once the manager has published its slot.
|
|
46
|
-
*/
|
|
47
|
-
export class PersonaManagerAwareness {
|
|
48
|
-
constructor(_awareness) {
|
|
49
|
-
this._awareness = _awareness;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Resolve once the `PersonaManager` has published its slot under the fixed
|
|
53
|
-
* client ID, polling the awareness map until it appears (or rejecting after a
|
|
54
|
-
* bounded wait). Pass the chat's Yjs awareness object (e.g. from the chat
|
|
55
|
-
* model's shared model).
|
|
56
|
-
*
|
|
57
|
-
* Safe to `await` in any context: if the slot is already present it resolves
|
|
58
|
-
* on the first check, effectively immediately.
|
|
59
|
-
*/
|
|
60
|
-
static async from(awareness) {
|
|
61
|
-
for (let i = 0; i < MAX_POLLS; i++) {
|
|
62
|
-
if (awareness.getStates().has(PERSONA_MANAGER_AWARENESS_CLIENT_ID)) {
|
|
63
|
-
return new PersonaManagerAwareness(awareness);
|
|
64
|
-
}
|
|
65
|
-
await delay(POLL_MS);
|
|
66
|
-
}
|
|
67
|
-
throw new Error('Timed out waiting for the PersonaManager awareness slot. Is the ' +
|
|
68
|
-
'jupyter-ai-persona-manager server extension installed and enabled?');
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* The personas available in this chat. Empty if none are published. Pass an
|
|
72
|
-
* entry to `PersonaAwareness.from()` to read that persona's own slot.
|
|
73
|
-
*/
|
|
74
|
-
get personas() {
|
|
75
|
-
const state = this._awareness
|
|
76
|
-
.getStates()
|
|
77
|
-
.get(PERSONA_MANAGER_AWARENESS_CLIENT_ID);
|
|
78
|
-
const personas = state === null || state === void 0 ? void 0 : state.personas;
|
|
79
|
-
return Array.isArray(personas) ? personas : [];
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* A typed, read-only view of one persona's awareness slot. Construct it with
|
|
84
|
-
* `PersonaAwareness.from(awareness, personaOption)` — the `PersonaOption` comes
|
|
85
|
-
* from `PersonaManagerAwareness.personas` and carries the persona's client ID.
|
|
86
|
-
*/
|
|
87
|
-
export class PersonaAwareness {
|
|
88
|
-
constructor(_awareness, _id, _clientId) {
|
|
89
|
-
this._awareness = _awareness;
|
|
90
|
-
this._id = _id;
|
|
91
|
-
this._clientId = _clientId;
|
|
92
|
-
}
|
|
93
|
-
/** Build a view of the persona named by `option` (from the persona list). */
|
|
94
|
-
static from(awareness, option) {
|
|
95
|
-
return new PersonaAwareness(awareness, option.id, option.yjs_client_id);
|
|
96
|
-
}
|
|
97
|
-
/** The persona's stable ID (from the persona list). */
|
|
98
|
-
get id() {
|
|
99
|
-
return this._id;
|
|
100
|
-
}
|
|
101
|
-
get _state() {
|
|
102
|
-
return this._awareness.getStates().get(this._clientId);
|
|
103
|
-
}
|
|
104
|
-
/** The persona's model configuration (current model, options, model settings). */
|
|
105
|
-
get model() {
|
|
106
|
-
var _a, _b;
|
|
107
|
-
return ((_b = (_a = this._state) === null || _a === void 0 ? void 0 : _a.model) !== null && _b !== void 0 ? _b : {
|
|
108
|
-
current: null,
|
|
109
|
-
options: [],
|
|
110
|
-
settings: []
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
/** The persona's general (non-model) setting configurations. */
|
|
114
|
-
get settings() {
|
|
115
|
-
var _a, _b;
|
|
116
|
-
return (_b = (_a = this._state) === null || _a === void 0 ? void 0 : _a.settings) !== null && _b !== void 0 ? _b : [];
|
|
117
|
-
}
|
|
118
|
-
/** The token and cost usage the persona reports for the session. */
|
|
119
|
-
get usage() {
|
|
120
|
-
var _a, _b;
|
|
121
|
-
// Backfill from EMPTY_USAGE so fields a (possibly older) server never
|
|
122
|
-
// published read as null, as the type promises, not undefined.
|
|
123
|
-
return {
|
|
124
|
-
...EMPTY_USAGE,
|
|
125
|
-
...((_b = (_a = this._state) === null || _a === void 0 ? void 0 : _a.usage) !== null && _b !== void 0 ? _b : {})
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
/** The slash commands the persona advertises. */
|
|
129
|
-
get slash_commands() {
|
|
130
|
-
var _a, _b;
|
|
131
|
-
return (_b = (_a = this._state) === null || _a === void 0 ? void 0 : _a.slash_commands) !== null && _b !== void 0 ? _b : [];
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Whether the persona is currently writing: `false` when idle, or the ID of
|
|
135
|
-
* the message being written while streaming.
|
|
136
|
-
*/
|
|
137
|
-
get isWriting() {
|
|
138
|
-
var _a, _b;
|
|
139
|
-
return (_b = (_a = this._state) === null || _a === void 0 ? void 0 : _a.isWriting) !== null && _b !== void 0 ? _b : false;
|
|
140
|
-
}
|
|
141
|
-
/** Whether this persona has published its state yet. */
|
|
142
|
-
get isReady() {
|
|
143
|
-
var _a;
|
|
144
|
-
return ((_a = this._state) === null || _a === void 0 ? void 0 : _a.model) !== undefined;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
22
|
+
export const EMPTY_MODEL_CONFIGURATION = {
|
|
23
|
+
current: null,
|
|
24
|
+
options: [],
|
|
25
|
+
settings: []
|
|
26
|
+
};
|
package/lib/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { JupyterFrontEndPlugin } from '@jupyterlab/application';
|
|
2
2
|
import { IInputToolbarRegistryFactory } from '@jupyter/chat';
|
|
3
3
|
import { IPersonaControlRegistry } from './persona-control-registry';
|
|
4
|
+
import { PersonaSessionRegistry } from './persona-events';
|
|
4
5
|
export * from './awareness';
|
|
6
|
+
export * from './persona-events';
|
|
5
7
|
export * from './persona-control-registry';
|
|
6
|
-
declare const _default: (JupyterFrontEndPlugin<void> | JupyterFrontEndPlugin<IPersonaControlRegistry> | JupyterFrontEndPlugin<IInputToolbarRegistryFactory>)[];
|
|
8
|
+
declare const _default: (JupyterFrontEndPlugin<void> | JupyterFrontEndPlugin<PersonaSessionRegistry> | JupyterFrontEndPlugin<IPersonaControlRegistry> | JupyterFrontEndPlugin<IInputToolbarRegistryFactory>)[];
|
|
7
9
|
export default _default;
|
package/lib/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { IChatCommandRegistry, IInputToolbarRegistryFactory, InputToolbarRegistry } from '@jupyter/chat';
|
|
2
|
+
import { IEventListener } from 'jupyterlab-eventlistener';
|
|
2
3
|
import { PersonaControls } from './persona-controls';
|
|
3
4
|
import { IPersonaControlRegistry, PersonaControlRegistry } from './persona-control-registry';
|
|
5
|
+
import { IPersonaSessionRegistry, PersonaSessionRegistry } from './persona-events';
|
|
4
6
|
import { SLASH_COMMAND_PROVIDER_ID, SlashCommandProvider } from './slash-commands';
|
|
5
7
|
import { StopButton } from './stop-button';
|
|
6
|
-
// Public
|
|
7
|
-
// persona awareness slots, for consumers building on the awareness channel.
|
|
8
|
+
// Public persona session data types (event payloads).
|
|
8
9
|
export * from './awareness';
|
|
10
|
+
// Public persona session-state models + registry (fed by Jupyter Events).
|
|
11
|
+
export * from './persona-events';
|
|
9
12
|
// Public API for contributing controls to the persona controls toolbar.
|
|
10
13
|
export * from './persona-control-registry';
|
|
11
14
|
/**
|
|
@@ -19,17 +22,31 @@ const plugin = {
|
|
|
19
22
|
console.log('JupyterLab extension @jupyter-ai/persona-manager is activated!');
|
|
20
23
|
}
|
|
21
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* Plugin that provides the shared per-chat persona session-state registry,
|
|
27
|
+
* fed by Jupyter Events via `jupyterlab-eventlistener`.
|
|
28
|
+
*/
|
|
29
|
+
const sessionRegistryPlugin = {
|
|
30
|
+
id: '@jupyter-ai/persona-manager:session-registry',
|
|
31
|
+
description: 'Provides the per-chat persona session-state registry (fed by Jupyter Events).',
|
|
32
|
+
autoStart: true,
|
|
33
|
+
provides: IPersonaSessionRegistry,
|
|
34
|
+
requires: [IEventListener],
|
|
35
|
+
activate: (app, eventListener) => {
|
|
36
|
+
return new PersonaSessionRegistry(eventListener);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
22
39
|
/**
|
|
23
40
|
* Plugin registering slash-command completions read from the selected
|
|
24
|
-
* persona's
|
|
41
|
+
* persona's session state (fed by Jupyter Events).
|
|
25
42
|
*/
|
|
26
43
|
const slashCommandPlugin = {
|
|
27
44
|
id: SLASH_COMMAND_PROVIDER_ID,
|
|
28
45
|
description: 'Adds support for slash commands in Jupyter AI.',
|
|
29
46
|
autoStart: true,
|
|
30
|
-
requires: [IChatCommandRegistry],
|
|
31
|
-
activate: (app, registry) => {
|
|
32
|
-
registry.addProvider(new SlashCommandProvider());
|
|
47
|
+
requires: [IChatCommandRegistry, IPersonaSessionRegistry],
|
|
48
|
+
activate: (app, registry, sessionRegistry) => {
|
|
49
|
+
registry.addProvider(new SlashCommandProvider(sessionRegistry));
|
|
33
50
|
}
|
|
34
51
|
};
|
|
35
52
|
/**
|
|
@@ -57,12 +74,11 @@ const toolbarPlugin = {
|
|
|
57
74
|
description: 'Provides the chat input toolbar with persona controls.',
|
|
58
75
|
autoStart: true,
|
|
59
76
|
provides: IInputToolbarRegistryFactory,
|
|
60
|
-
requires: [IPersonaControlRegistry],
|
|
61
|
-
activate: (app, controlRegistry) => {
|
|
62
|
-
// Wrap the persona controls to inject the control
|
|
63
|
-
// generic toolbar-item props don't carry.
|
|
64
|
-
|
|
65
|
-
const PersonaControlsItem = (itemProps) => PersonaControls({ ...itemProps, controlRegistry });
|
|
77
|
+
requires: [IPersonaControlRegistry, IPersonaSessionRegistry],
|
|
78
|
+
activate: (app, controlRegistry, sessionRegistry) => {
|
|
79
|
+
// Wrap the persona controls to inject the control + session registries,
|
|
80
|
+
// which the generic toolbar-item props don't carry.
|
|
81
|
+
const PersonaControlsItem = (itemProps) => PersonaControls({ ...itemProps, controlRegistry, sessionRegistry });
|
|
66
82
|
return {
|
|
67
83
|
create: () => {
|
|
68
84
|
// Start with the default toolbar (Send, Attach, Cancel, SaveEdit)
|
|
@@ -86,6 +102,7 @@ const toolbarPlugin = {
|
|
|
86
102
|
};
|
|
87
103
|
export default [
|
|
88
104
|
plugin,
|
|
105
|
+
sessionRegistryPlugin,
|
|
89
106
|
slashCommandPlugin,
|
|
90
107
|
controlRegistryPlugin,
|
|
91
108
|
toolbarPlugin
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { InputToolbarRegistry } from '@jupyter/chat';
|
|
2
|
-
import {
|
|
2
|
+
import { PersonaOption, Usage } from './awareness';
|
|
3
|
+
import { PersonaSessionRegistry, PersonaSessionState } from './persona-events';
|
|
3
4
|
import { PersonaSettings } from './metadata';
|
|
4
5
|
import { IPersonaControlRegistry } from './persona-control-registry';
|
|
5
6
|
/**
|
|
@@ -26,7 +27,7 @@ export type Control = {
|
|
|
26
27
|
* persona advertises models), its model settings, then its general settings.
|
|
27
28
|
* The user's current selection seeds each control's `selection`.
|
|
28
29
|
*/
|
|
29
|
-
export declare function buildControls(persona:
|
|
30
|
+
export declare function buildControls(persona: PersonaSessionState | null, settings: PersonaSettings): Control[];
|
|
30
31
|
/**
|
|
31
32
|
* Decide which persona list the toolbar should display: a freshly read empty
|
|
32
33
|
* list is treated as a transient blip (e.g. a Yjs awareness sync hiccup
|
|
@@ -56,7 +57,7 @@ export declare function reconcilePersonas(previous: PersonaOption[], next: Perso
|
|
|
56
57
|
* result, or switching personas could show the previous persona's stale
|
|
57
58
|
* state under the new selection.
|
|
58
59
|
*/
|
|
59
|
-
export declare function reconcilePersonaState(previous:
|
|
60
|
+
export declare function reconcilePersonaState(previous: PersonaSessionState | null, next: PersonaSessionState | null): PersonaSessionState | null;
|
|
60
61
|
/**
|
|
61
62
|
* Decide how to reconcile the current selection with a freshly read persona
|
|
62
63
|
* list: the new selection to apply, or `undefined` to keep the current one.
|
|
@@ -171,5 +172,10 @@ export declare function PersonaControls(props: InputToolbarRegistry.IToolbarItem
|
|
|
171
172
|
* Optional so the component still works without a registry.
|
|
172
173
|
*/
|
|
173
174
|
controlRegistry?: IPersonaControlRegistry;
|
|
175
|
+
/**
|
|
176
|
+
* The per-chat persona session-state registry (fed by Jupyter Events).
|
|
177
|
+
* Optional so the component still works without it (renders nothing).
|
|
178
|
+
*/
|
|
179
|
+
sessionRegistry?: PersonaSessionRegistry;
|
|
174
180
|
}): JSX.Element | null;
|
|
175
181
|
export {};
|
package/lib/persona-controls.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { Button, ListItemText, ListSubheader, Menu, MenuItem, Popover, Skeleton, TextField } from '@mui/material';
|
|
3
3
|
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
|
4
4
|
import CheckIcon from '@mui/icons-material/Check';
|
|
5
5
|
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
|
6
6
|
import { PageConfig } from '@jupyterlab/coreutils';
|
|
7
|
-
import { EMPTY_USAGE
|
|
7
|
+
import { EMPTY_USAGE } from './awareness';
|
|
8
8
|
import { buildMessageMetadata, emptyPersonaSettings } from './metadata';
|
|
9
9
|
const SELECTOR_CLASS = 'jp-jai-personaControls';
|
|
10
10
|
const MENU_CLASS = 'jp-jai-controlMenu';
|
|
@@ -635,20 +635,16 @@ export function UsageChip(props) {
|
|
|
635
635
|
*/
|
|
636
636
|
export function PersonaControls(props) {
|
|
637
637
|
var _a, _b, _c, _d, _e, _f;
|
|
638
|
-
const { chatModel, model, controlRegistry } = props;
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
//
|
|
642
|
-
//
|
|
643
|
-
const
|
|
644
|
-
// Whether resolving the manager's slot failed (timed out, extension absent).
|
|
645
|
-
// Hides the loading placeholder along with the toolbar.
|
|
646
|
-
const [managerFailed, setManagerFailed] = useState(false);
|
|
647
|
-
// Whether the first persona-list read has completed after the manager
|
|
648
|
-
// resolved. Before that, an empty list means "still loading", not "this chat
|
|
649
|
-
// has no personas".
|
|
650
|
-
const [listRead, setListRead] = useState(false);
|
|
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;
|
|
641
|
+
// The per-chat persona session state, built from persona events and shared
|
|
642
|
+
// via the registry. Created on demand; discarded when the chat closes.
|
|
643
|
+
const managerState = useMemo(() => (sessionRegistry && path ? sessionRegistry.get(path) : null), [sessionRegistry, path]);
|
|
651
644
|
const [personas, setPersonas] = useState([]);
|
|
645
|
+
// Whether a persona list has been received for this chat yet. Before that, an
|
|
646
|
+
// empty list means "still loading", not "this chat has no personas".
|
|
647
|
+
const [ready, setReady] = useState(false);
|
|
652
648
|
const [selectedId, setSelectedId] = useState(DEFAULT_PERSONA_ID);
|
|
653
649
|
const [personaState, setPersonaState] = useState(null);
|
|
654
650
|
// Per-persona settings the user has chosen, indexed by persona ID. Remembers
|
|
@@ -665,86 +661,66 @@ export function PersonaControls(props) {
|
|
|
665
661
|
const settings = selectedId
|
|
666
662
|
? ((_b = settingsCache[selectedId]) !== null && _b !== void 0 ? _b : emptyPersonaSettings())
|
|
667
663
|
: emptyPersonaSettings();
|
|
668
|
-
//
|
|
669
|
-
|
|
670
|
-
if (!awareness) {
|
|
671
|
-
return;
|
|
672
|
-
}
|
|
673
|
-
let cancelled = false;
|
|
674
|
-
PersonaManagerAwareness.from(awareness)
|
|
675
|
-
.then(pm => {
|
|
676
|
-
if (!cancelled) {
|
|
677
|
-
setManager(pm);
|
|
678
|
-
}
|
|
679
|
-
})
|
|
680
|
-
.catch(reason => {
|
|
681
|
-
// Manager never registered (e.g. extension disabled); the toolbar
|
|
682
|
-
// stays hidden. Surface why, or the empty toolbar is undiagnosable.
|
|
683
|
-
console.warn('Persona toolbar hidden:', reason);
|
|
684
|
-
if (!cancelled) {
|
|
685
|
-
setManagerFailed(true);
|
|
686
|
-
}
|
|
687
|
-
});
|
|
688
|
-
return () => {
|
|
689
|
-
cancelled = true;
|
|
690
|
-
};
|
|
691
|
-
}, [awareness]);
|
|
692
|
-
// Re-read the persona list from the manager view and reconcile the selection
|
|
693
|
-
// (see reconcileSelection for the decision rules). This is the reactive
|
|
694
|
-
// plumbing that replaces polling: a persona publishing or updating its state
|
|
695
|
-
// fires an awareness `change` event.
|
|
664
|
+
// Re-read the persona list from the session state and reconcile the
|
|
665
|
+
// selection (see reconcileSelection for the decision rules).
|
|
696
666
|
const readManager = useCallback(() => {
|
|
697
|
-
if (!
|
|
667
|
+
if (!managerState) {
|
|
698
668
|
return;
|
|
699
669
|
}
|
|
700
|
-
const list =
|
|
670
|
+
const list = managerState.personas;
|
|
701
671
|
setPersonas(prev => reconcilePersonas(prev, list));
|
|
702
672
|
setSelectedId(current => {
|
|
703
673
|
const next = reconcileSelection(list, current, userPicked.current);
|
|
704
674
|
return next === undefined ? current : next;
|
|
705
675
|
});
|
|
706
|
-
}, [
|
|
676
|
+
}, [managerState]);
|
|
677
|
+
// React to the session state's `changed` signal: re-read the persona list and
|
|
678
|
+
// readiness. This replaces the awareness `change` subscription — a persona
|
|
679
|
+
// publishing or updating its state fires `changed`.
|
|
707
680
|
useEffect(() => {
|
|
708
|
-
if (!
|
|
681
|
+
if (!managerState) {
|
|
709
682
|
return;
|
|
710
683
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
684
|
+
const sync = () => {
|
|
685
|
+
readManager();
|
|
686
|
+
setReady(managerState.ready);
|
|
687
|
+
};
|
|
688
|
+
sync();
|
|
689
|
+
managerState.changed.connect(sync);
|
|
715
690
|
return () => {
|
|
716
|
-
|
|
691
|
+
managerState.changed.disconnect(sync);
|
|
717
692
|
};
|
|
718
|
-
}, [
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
}
|
|
725
|
-
const option = manager.personas.find(p => p.id === selectedId);
|
|
726
|
-
return option ? PersonaAwareness.from(awareness, option) : null;
|
|
727
|
-
};
|
|
728
|
-
// Track the selected persona's view in state, re-reading on every awareness
|
|
729
|
-
// change (a persona updating usage, model, or commands) so the toolbar
|
|
730
|
-
// reflects the latest published state. The first read (right below)
|
|
731
|
-
// applies unconditionally, since it runs whenever `selectedId` itself
|
|
732
|
-
// changes and must reflect the newly selected persona, even a genuinely
|
|
733
|
-
// absent one; every later read goes through reconcilePersonaState so a
|
|
734
|
-
// transient blip doesn't blank this out - see its comment for why that
|
|
735
|
-
// matters more than it looks like it should.
|
|
693
|
+
}, [managerState, readManager]);
|
|
694
|
+
// Track the selected persona's state, re-reading on every change so the
|
|
695
|
+
// toolbar reflects the latest published model/usage/commands. The first read
|
|
696
|
+
// applies unconditionally (it runs whenever `selectedId` changes and must
|
|
697
|
+
// reflect the newly selected persona, even an absent one); later reads go
|
|
698
|
+
// through reconcilePersonaState so a transient absence doesn't blank it out.
|
|
736
699
|
useEffect(() => {
|
|
737
|
-
|
|
700
|
+
var _a;
|
|
701
|
+
if (!managerState || !selectedId) {
|
|
738
702
|
setPersonaState(null);
|
|
739
703
|
return;
|
|
740
704
|
}
|
|
741
|
-
setPersonaState(
|
|
742
|
-
const read = () => setPersonaState(prev => reconcilePersonaState(prev,
|
|
743
|
-
|
|
705
|
+
setPersonaState((_a = managerState.getPersona(selectedId)) !== null && _a !== void 0 ? _a : null);
|
|
706
|
+
const read = () => setPersonaState(prev => { var _a; return reconcilePersonaState(prev, (_a = managerState.getPersona(selectedId)) !== null && _a !== void 0 ? _a : null); });
|
|
707
|
+
managerState.changed.connect(read);
|
|
708
|
+
return () => {
|
|
709
|
+
managerState.changed.disconnect(read);
|
|
710
|
+
};
|
|
711
|
+
}, [managerState, selectedId]);
|
|
712
|
+
// Discard this chat's session state when the chat model is disposed (chat
|
|
713
|
+
// closed), freeing its memory.
|
|
714
|
+
useEffect(() => {
|
|
715
|
+
if (!sessionRegistry || !path || !chatModel) {
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
const onDisposed = () => sessionRegistry.discard(path);
|
|
719
|
+
chatModel.disposed.connect(onDisposed);
|
|
744
720
|
return () => {
|
|
745
|
-
|
|
721
|
+
chatModel.disposed.disconnect(onDisposed);
|
|
746
722
|
};
|
|
747
|
-
}, [
|
|
723
|
+
}, [sessionRegistry, path, chatModel]);
|
|
748
724
|
// Stamp the current persona + its settings onto the input model's metadata,
|
|
749
725
|
// so it rides out with the next message and the PersonaManager routes and
|
|
750
726
|
// applies it. Keyed on a signature so we only write when it changes.
|
|
@@ -755,9 +731,12 @@ export function PersonaControls(props) {
|
|
|
755
731
|
}, [model, metadataSignature]);
|
|
756
732
|
// No personas yet. While the manager's slot or its first list read is still
|
|
757
733
|
// pending, show a loading placeholder (on slow networks this takes seconds);
|
|
758
|
-
// once
|
|
734
|
+
// once ready with no personas, show nothing.
|
|
759
735
|
if (!personas.length) {
|
|
760
|
-
|
|
736
|
+
// Still loading while the session state exists but no persona list has
|
|
737
|
+
// arrived yet (events are in flight). Once ready with an empty list, or
|
|
738
|
+
// with no registry at all, render nothing.
|
|
739
|
+
if (managerState && !ready) {
|
|
761
740
|
return React.createElement(LoadingPlaceholder, null);
|
|
762
741
|
}
|
|
763
742
|
return null;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { IEventListener } from 'jupyterlab-eventlistener';
|
|
2
|
+
import { Token } from '@lumino/coreutils';
|
|
3
|
+
import { IDisposable } from '@lumino/disposable';
|
|
4
|
+
import { ISignal } from '@lumino/signaling';
|
|
5
|
+
import { CommandOption, ModelConfiguration, PersonaOption, SettingConfiguration, Usage } from './awareness';
|
|
6
|
+
export declare const PERSONAS_EVENT_SCHEMA_ID = "https://schema.jupyter.org/jupyter_ai_persona_manager/personas/v1";
|
|
7
|
+
export declare const PERSONA_STATE_EVENT_SCHEMA_ID = "https://schema.jupyter.org/jupyter_ai_persona_manager/persona_state/v1";
|
|
8
|
+
/** The wire shape of a `persona_state` event. */
|
|
9
|
+
type PersonaStatePayload = {
|
|
10
|
+
path?: string;
|
|
11
|
+
persona_id?: string;
|
|
12
|
+
model?: ModelConfiguration;
|
|
13
|
+
settings?: SettingConfiguration[];
|
|
14
|
+
usage?: Usage;
|
|
15
|
+
slash_commands?: CommandOption[];
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* One persona's live session state, built from a `persona_state` event.
|
|
19
|
+
* Immutable: each update produces a new instance, so React consumers see a new
|
|
20
|
+
* reference and re-render.
|
|
21
|
+
*/
|
|
22
|
+
export declare class PersonaSessionState {
|
|
23
|
+
readonly id: string;
|
|
24
|
+
constructor(id: string, payload?: PersonaStatePayload);
|
|
25
|
+
readonly model: ModelConfiguration;
|
|
26
|
+
readonly settings: SettingConfiguration[];
|
|
27
|
+
readonly usage: Usage;
|
|
28
|
+
readonly slash_commands: CommandOption[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The per-chat manager session state: the persona list plus a
|
|
32
|
+
* `PersonaSessionState` per persona. Fires `changed` whenever the persona list
|
|
33
|
+
* or any persona's state updates, so React components re-render.
|
|
34
|
+
*/
|
|
35
|
+
export declare class PersonaManagerSessionState implements IDisposable {
|
|
36
|
+
readonly path: string;
|
|
37
|
+
constructor(path: string);
|
|
38
|
+
/** Emits whenever the persona list or a persona's state changes. */
|
|
39
|
+
get changed(): ISignal<this, void>;
|
|
40
|
+
/** Whether a `personas` event has been received for this chat yet. */
|
|
41
|
+
get ready(): boolean;
|
|
42
|
+
/** The personas advertised in this chat. */
|
|
43
|
+
get personas(): PersonaOption[];
|
|
44
|
+
/** A persona's session state, or undefined if it has not published yet. */
|
|
45
|
+
getPersona(id: string): PersonaSessionState | undefined;
|
|
46
|
+
/** Apply a `personas` event payload. */
|
|
47
|
+
updatePersonas(personas: PersonaOption[]): void;
|
|
48
|
+
/** Apply a `persona_state` event payload for one persona. */
|
|
49
|
+
updatePersonaState(personaId: string, payload: PersonaStatePayload): void;
|
|
50
|
+
get isDisposed(): boolean;
|
|
51
|
+
dispose(): void;
|
|
52
|
+
private _personasReceived;
|
|
53
|
+
private _personas;
|
|
54
|
+
private _states;
|
|
55
|
+
private _isDisposed;
|
|
56
|
+
private _changed;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Routes persona events to the correct per-chat `PersonaManagerSessionState`,
|
|
60
|
+
* creating one on demand and discarding it when the chat closes.
|
|
61
|
+
*
|
|
62
|
+
* A single instance is created by the plugin and shared with the toolbar
|
|
63
|
+
* controls and the slash-command provider.
|
|
64
|
+
*/
|
|
65
|
+
export declare class PersonaSessionRegistry {
|
|
66
|
+
constructor(eventListener: IEventListener);
|
|
67
|
+
/**
|
|
68
|
+
* Get (or create) the manager session state for a chat path. Components call
|
|
69
|
+
* this with their chat's `IChatModel.name`.
|
|
70
|
+
*/
|
|
71
|
+
get(path: string): PersonaManagerSessionState;
|
|
72
|
+
/** Whether a manager session state exists for `path` (without creating one). */
|
|
73
|
+
has(path: string): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Discard a chat's session state and free its memory. Called when the client
|
|
76
|
+
* closes the chat (wired to the chat model's `disposed` signal).
|
|
77
|
+
*/
|
|
78
|
+
discard(path: string): void;
|
|
79
|
+
private _onPersonasEvent;
|
|
80
|
+
private _onPersonaStateEvent;
|
|
81
|
+
private _byPath;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Plugin token for the shared `PersonaSessionRegistry`. Consumed by the toolbar
|
|
85
|
+
* controls and the slash-command provider.
|
|
86
|
+
*/
|
|
87
|
+
export declare const IPersonaSessionRegistry: Token<PersonaSessionRegistry>;
|
|
88
|
+
export {};
|