jupyterlab_ai_code_assistants_extension 0.6.17 → 1.0.17
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 +5 -2
- package/lib/core/colour.d.ts +101 -8
- package/lib/core/colour.js +218 -50
- package/lib/core/icons.js +8 -2
- package/lib/core/limits.d.ts +12 -0
- package/lib/core/limits.js +12 -0
- package/lib/core/modes.d.ts +19 -0
- package/lib/core/modes.js +36 -0
- package/lib/core/panel.d.ts +111 -6
- package/lib/core/panel.js +717 -133
- package/lib/core/popup.d.ts +4 -0
- package/lib/core/popup.js +5 -2
- package/lib/core/registry.d.ts +4 -1
- package/lib/core/registry.js +4 -1
- package/lib/core/request.d.ts +47 -0
- package/lib/core/request.js +97 -6
- package/lib/core/terminals.d.ts +14 -4
- package/lib/core/terminals.js +86 -20
- package/lib/core/types.d.ts +39 -36
- package/lib/index.js +45 -10
- package/lib/providers/claude.js +4 -19
- package/lib/providers/codex.js +2 -2
- package/lib/providers/gemini.js +7 -5
- package/lib/providers/kimi.js +3 -3
- package/package.json +1 -2
- package/src/__tests__/colour.spec.ts +451 -1
- package/src/__tests__/core-neutrality.spec.ts +142 -10
- package/src/__tests__/launch-mode.spec.ts +50 -3
- package/src/__tests__/panel.spec.ts +623 -0
- package/src/__tests__/registry.spec.ts +2 -51
- package/src/__tests__/request.spec.ts +197 -0
- package/src/core/colour.ts +234 -60
- package/src/core/icons.ts +8 -2
- package/src/core/limits.ts +12 -0
- package/src/core/modes.ts +48 -0
- package/src/core/panel.ts +816 -141
- package/src/core/popup.ts +9 -2
- package/src/core/registry.ts +4 -1
- package/src/core/request.ts +107 -11
- package/src/core/terminals.ts +85 -19
- package/src/core/types.ts +39 -40
- package/src/index.ts +53 -8
- package/src/providers/claude.ts +5 -27
- package/src/providers/codex.ts +3 -7
- package/src/providers/gemini.ts +7 -5
- package/src/providers/kimi.ts +3 -3
- package/style/base.css +60 -23
package/README.md
CHANGED
|
@@ -33,13 +33,14 @@ Chat-panel extensions re-implement the agent loop and trail the real tool. This
|
|
|
33
33
|
- **Conversation switcher** - a right-click "Switch and Manage Sessions" submenu lists a project's other conversations by name and short id with last-activity time; "Manage Sessions..." opens a searchable popup over the full list with multi-select delete and per-row open and copy-id buttons
|
|
34
34
|
- **Branch session** - fork the current conversation into a new named session via the right-click menu; each assistant forks its own way (Claude's native `--fork-session`, Codex's `codex fork`, server-side copies for Kimi and Gemini) behind the same menu item
|
|
35
35
|
- **Launch modes under each assistant's own name** - Claude's skip-permissions, Codex's approval bypass, Kimi's `--yolo`, Gemini's YOLO and approval modes; unsafe variants carry a warning glyph
|
|
36
|
-
- **Coloured terminal tabs** - each session's colour tints its terminal tab via the companion `jupyterlab_colourful_tab_extension` (installed automatically).
|
|
36
|
+
- **Coloured terminal tabs** - each session's colour tints its terminal tab via the companion `jupyterlab_colourful_tab_extension` (installed automatically). Claude's own `/color` supplies its default, Kimi derives a stable colour from the conversation id; Codex and Gemini have no colour of their own until you set one, and a branched session inherits its parent's colour, which it keeps even if the parent's colour later changes or is reset
|
|
37
|
+
- **Your own tab colour wins** - set a colour on a terminal tab and the extension remembers it for that conversation, overriding whatever the assistant chose. `Reset Tab Colour (n)` in the session's right-click menu hands back every colour you set by hand on that project's conversations, and appears once there is one to hand back. Use it rather than the tab menu's own Clear, which will not release a stored colour. A Codex or Kimi conversation that has never been resumed cannot be tracked yet, so a colour set on its tab is not kept
|
|
37
38
|
- **Favorites** - star projects you keep coming back to via the right-click menu; favourites from the standalone extensions are migrated on first run
|
|
38
39
|
- **Remove and clean up** - drop a project's history or a project's extra parallel sessions from the right-click menu, confirmation dialog first; removed files honour JupyterLab's "move files to trash" setting
|
|
39
40
|
- **Activity at a glance** - each row shows its last activity in an aligned column; rows active within the last minute light up, rows idle for over a week dim, and rows with parallel conversations show a branch icon with the count
|
|
40
41
|
- **Remote control indicator and background agents** (Claude) - a green dot marks sessions actively under remote control, and a conversation held by a running background agent shows a `bg` chip; clicking attaches to the agent instead of copying it
|
|
41
42
|
- **Search** - fuzzy filter per panel, toggled by the funnel button
|
|
42
|
-
- **Presentation modes** - label rows by session name
|
|
43
|
+
- **Presentation modes** - label rows by session name or by path relative to the JupyterLab root
|
|
43
44
|
- **Conflict-safe upgrade** - if a retired standalone extension is still installed, its panel wins and this extension stands down for that assistant instead of showing a duplicate
|
|
44
45
|
- **Auto-disabled when absent** - an assistant whose CLI is not on `PATH` does not show a panel
|
|
45
46
|
|
|
@@ -74,6 +75,8 @@ This package supersedes `jupyterlab_claude_code_extension`, `jupyterlab_codex_ex
|
|
|
74
75
|
pip uninstall jupyterlab_claude_code_extension jupyterlab_codex_extension jupyterlab_kimi_code_extension
|
|
75
76
|
```
|
|
76
77
|
|
|
78
|
+
Tab colours stored by versions of THIS extension before 1.0.0 are carried over and keep tinting their tabs, but `Reset Tab Colour` will not offer them: the store from that version cannot tell a colour you set from one a branch inherited, and offering the wrong one would strip a branch of the colour it was forked with. Set the colour on that tab once more and it becomes yours to reset.
|
|
79
|
+
|
|
77
80
|
While a standalone extension is still installed, its panel is shown and this extension's panel for that assistant stays out of the way.
|
|
78
81
|
|
|
79
82
|
## Uninstall
|
package/lib/core/colour.d.ts
CHANGED
|
@@ -8,14 +8,24 @@ export declare const TAB_COLOUR_IDS: readonly string[];
|
|
|
8
8
|
/** Map a conversation id onto one of the six colour ids. FNV-1a (32-bit) - a
|
|
9
9
|
* stable string hash with good avalanche on short hex-ish ids; `Math.imul`
|
|
10
10
|
* keeps the multiplication in 32-bit integer semantics. The same id always
|
|
11
|
-
* yields the same colour, on every reload and in every browser.
|
|
11
|
+
* yields the same colour, on every reload and in every browser.
|
|
12
|
+
*
|
|
13
|
+
* Hashed over the id's UTF-8 BYTES, which is what FNV-1a is defined over and
|
|
14
|
+
* what the Python half hashes too. The natural idiom on each side disagrees:
|
|
15
|
+
* `charCodeAt` walks UTF-16 units and Python's `ord` walks code points, so an
|
|
16
|
+
* id outside the BMP hashed differently on the two runtimes - measured, "𝕏id"
|
|
17
|
+
* tinted rose in the browser and peach on the server (DEF-52). Bytes are the
|
|
18
|
+
* one reading both languages spell naturally, so the drift cannot come back
|
|
19
|
+
* the next time somebody rewrites either loop. Every real conversation id is
|
|
20
|
+
* ASCII, where all three readings coincide, so no existing tab changes colour. */
|
|
12
21
|
export declare function fnv1aColour(sessionId: string): string;
|
|
13
22
|
/** Colour the user set by hand on a terminal tab, or null.
|
|
14
23
|
*
|
|
15
24
|
* The colourful-tab extension stores its right-click menu choices as
|
|
16
25
|
* `{ "terminal:<session name>": <index into TAB_COLOUR_IDS> }`. Reading it is
|
|
17
26
|
* what makes "set the colour on the tab" register as the conversation's
|
|
18
|
-
* colour for
|
|
27
|
+
* colour - for every assistant, including one whose CLI has a colour concept
|
|
28
|
+
* of its own. */
|
|
19
29
|
export declare function readUserSetTabColour(terminalName: string): string | null;
|
|
20
30
|
/** Resolve the ladder for one conversation. `userSet` is this extension's own
|
|
21
31
|
* store, `native` the conversation's own colour (only ever populated by a
|
|
@@ -34,24 +44,107 @@ export declare function sessionForCwds(cwds: string[], sessions: ISession[]): IS
|
|
|
34
44
|
* colours survive a reload and one assistant's colours can never be read or
|
|
35
45
|
* written through another's routes. Reads are cached in memory and refreshed
|
|
36
46
|
* with the session poll; writes go straight through and update the cache.
|
|
47
|
+
*
|
|
48
|
+
* Every request this store makes is queued behind the last - see `_run`. That
|
|
49
|
+
* one rule is what keeps the class small: with only one of its requests ever on
|
|
50
|
+
* the wire, no answer can be older than something the cache has already taken,
|
|
51
|
+
* so none of the ordering questions this class used to carry can be asked.
|
|
37
52
|
*/
|
|
38
53
|
export declare class ColourStore {
|
|
39
54
|
constructor(providerId: string, serverSettings: ServerConnection.ISettings);
|
|
40
55
|
/** User-set colour for a conversation, from the in-memory cache. */
|
|
41
56
|
get(sessionId: string | null | undefined): string | null;
|
|
57
|
+
/** Whether this conversation's colour is the user's own choice rather than
|
|
58
|
+
* one it inherited at fork time. Only a hand-set colour is offered for
|
|
59
|
+
* release: dropping an inherited one would un-inherit the branch. */
|
|
60
|
+
isOverride(sessionId: string | null | undefined): boolean;
|
|
61
|
+
/** Whether a write against this conversation is still unanswered - queued
|
|
62
|
+
* behind an earlier request of this store's, or on the wire.
|
|
63
|
+
*
|
|
64
|
+
* Read by the release affordance - see the filter in `_openContextMenu` for
|
|
65
|
+
* why it must not offer a colour still being written. Counted from the CALL
|
|
66
|
+
* rather than from the moment the request goes out: with the store
|
|
67
|
+
* serialised (DEF-30) a capture can sit in the queue before it is sent, and a
|
|
68
|
+
* release offered in that window is undone by the capture landing after it.
|
|
69
|
+
*
|
|
70
|
+
* Not consulted by the capture gate - the cache holds only confirmed values,
|
|
71
|
+
* so reading it is already the confirmation. */
|
|
72
|
+
isPending(sessionId: string | null | undefined): boolean;
|
|
42
73
|
/** Reload the whole store. Failures leave the previous cache in place - a
|
|
43
|
-
* momentarily unreachable server must not clear every user colour.
|
|
74
|
+
* momentarily unreachable server must not clear every user colour.
|
|
75
|
+
*
|
|
76
|
+
* The answer is taken whole. It is the only answer whose snapshot the caller
|
|
77
|
+
* ordered, and nothing this store wrote can be missing from it: the GET is
|
|
78
|
+
* not sent until every earlier write of this store's has been answered. */
|
|
44
79
|
load(): Promise<void>;
|
|
80
|
+
/** Queue one request behind every request this store has already issued, and
|
|
81
|
+
* answer what it answers.
|
|
82
|
+
*
|
|
83
|
+
* This is the store's entire ordering discipline (DEF-30). Only one of its
|
|
84
|
+
* requests is ever on the wire, so an answer can never describe a moment
|
|
85
|
+
* older than one the cache has already taken. The monotonic write counter,
|
|
86
|
+
* the per-id record of the latest write and the reload generation that used
|
|
87
|
+
* to live here could only ever describe interleavings that are now
|
|
88
|
+
* unconstructible.
|
|
89
|
+
*
|
|
90
|
+
* A failed request must not poison the queue - one refused write stopping
|
|
91
|
+
* every later load, capture and release for the rest of the session is the
|
|
92
|
+
* same defect this change removes, relocated. Two things hold that, and only
|
|
93
|
+
* the first is reachable by test: each op catches its own failure and answers
|
|
94
|
+
* false, so no link rejects today; and the tail waits for SETTLEMENT rather
|
|
95
|
+
* than success, so `_chain` still cannot reject if a later edit puts throwing
|
|
96
|
+
* code outside an op's own `try` - `forget`'s filter already sits there.
|
|
97
|
+
*
|
|
98
|
+
* `requestAPI` bounds every request at `REQUEST_TIMEOUT_MS` (DEF-72), so a
|
|
99
|
+
* queued op waits a bounded time even against a server that never answers.
|
|
100
|
+
* The ceiling is per request, so K ops queued behind a dead server wait K
|
|
101
|
+
* times it, not it.
|
|
102
|
+
*
|
|
103
|
+
* An op must never call `load`, `set` or `forget` - it would wait on a chain
|
|
104
|
+
* that is waiting on it. `inherit` delegates to `set` from OUTSIDE the queue,
|
|
105
|
+
* which is why it is not itself queued. */
|
|
106
|
+
private _run;
|
|
107
|
+
/** Open a write against one or more ids. Counted, not flagged: two writes to
|
|
108
|
+
* one id can be queued at once, and a flag would let the first to settle
|
|
109
|
+
* clear the second's guard. */
|
|
110
|
+
private _openWrite;
|
|
111
|
+
/** Close it. */
|
|
112
|
+
private _closeWrite;
|
|
45
113
|
/** Record a colour as this conversation's own. Passing null drops the entry,
|
|
46
|
-
* which is also how a deleted conversation leaves no orphan key behind.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
114
|
+
* which is also how a deleted conversation leaves no orphan key behind.
|
|
115
|
+
* `handSet` false marks it inherited - written on a fork's behalf rather
|
|
116
|
+
* than chosen on its tab.
|
|
117
|
+
*
|
|
118
|
+
* Answers whether the store now HOLDS the colour, which is what
|
|
119
|
+
* `reconcileColours` gates its paint on.
|
|
120
|
+
*
|
|
121
|
+
* The cache is written only once the server has confirmed. Writing it
|
|
122
|
+
* eagerly and undoing that on failure looks cheaper and is not: nothing here
|
|
123
|
+
* reads the cache before this promise settles, while an unconfirmed entry is
|
|
124
|
+
* indistinguishable from a stored one to every reader - which is exactly the
|
|
125
|
+
* mistake the gate above exists to catch.
|
|
126
|
+
*
|
|
127
|
+
* "Did not throw" is not the test - the answer carries the whole store, so
|
|
128
|
+
* it is checked for the value that was just written. A server that accepted
|
|
129
|
+
* the request and then failed to persist it would otherwise be believed. */
|
|
130
|
+
set(sessionId: string, colour: string | null, handSet?: boolean): Promise<boolean>;
|
|
131
|
+
/** Drop stored colours - conversations that no longer exist, or overrides
|
|
132
|
+
* the user released. One request for the whole list, so a release is never
|
|
133
|
+
* left half applied. Same contract as `set`: the answer is checked for the
|
|
134
|
+
* absence of every id, and the cache is touched only once it agrees. */
|
|
135
|
+
forget(sessionIds: string[]): Promise<boolean>;
|
|
50
136
|
/** Copy the parent's EFFECTIVE colour onto a fresh branch, so a branch of a
|
|
51
137
|
* branch inherits whatever its immediate parent actually shows, user-set
|
|
52
|
-
* override included. Called at the fork sites, once the new id is known.
|
|
138
|
+
* override included. Called at the fork sites, once the new id is known.
|
|
139
|
+
*/
|
|
53
140
|
inherit(parentColour: string | null, childId: string): Promise<void>;
|
|
54
141
|
private readonly _providerId;
|
|
55
142
|
private readonly _serverSettings;
|
|
56
143
|
private _colours;
|
|
144
|
+
private _overrides;
|
|
145
|
+
/** Unanswered writes per id, counted from the call. Read by the release
|
|
146
|
+
* affordance, which must not offer a colour still being written. */
|
|
147
|
+
private _pending;
|
|
148
|
+
/** Tail of this store's request queue - see `_run`. Never rejects. */
|
|
149
|
+
private _chain;
|
|
57
150
|
}
|
package/lib/core/colour.js
CHANGED
|
@@ -29,11 +29,20 @@ const TAB_COLOUR_STORAGE_KEY = 'jupyterlab-colourful-tab-colours';
|
|
|
29
29
|
/** Map a conversation id onto one of the six colour ids. FNV-1a (32-bit) - a
|
|
30
30
|
* stable string hash with good avalanche on short hex-ish ids; `Math.imul`
|
|
31
31
|
* keeps the multiplication in 32-bit integer semantics. The same id always
|
|
32
|
-
* yields the same colour, on every reload and in every browser.
|
|
32
|
+
* yields the same colour, on every reload and in every browser.
|
|
33
|
+
*
|
|
34
|
+
* Hashed over the id's UTF-8 BYTES, which is what FNV-1a is defined over and
|
|
35
|
+
* what the Python half hashes too. The natural idiom on each side disagrees:
|
|
36
|
+
* `charCodeAt` walks UTF-16 units and Python's `ord` walks code points, so an
|
|
37
|
+
* id outside the BMP hashed differently on the two runtimes - measured, "𝕏id"
|
|
38
|
+
* tinted rose in the browser and peach on the server (DEF-52). Bytes are the
|
|
39
|
+
* one reading both languages spell naturally, so the drift cannot come back
|
|
40
|
+
* the next time somebody rewrites either loop. Every real conversation id is
|
|
41
|
+
* ASCII, where all three readings coincide, so no existing tab changes colour. */
|
|
33
42
|
export function fnv1aColour(sessionId) {
|
|
34
43
|
let hash = 0x811c9dc5; // FNV offset basis
|
|
35
|
-
for (
|
|
36
|
-
hash ^=
|
|
44
|
+
for (const byte of new TextEncoder().encode(sessionId)) {
|
|
45
|
+
hash ^= byte;
|
|
37
46
|
hash = Math.imul(hash, 0x01000193); // FNV prime
|
|
38
47
|
}
|
|
39
48
|
return TAB_COLOUR_IDS[(hash >>> 0) % TAB_COLOUR_IDS.length];
|
|
@@ -43,7 +52,8 @@ export function fnv1aColour(sessionId) {
|
|
|
43
52
|
* The colourful-tab extension stores its right-click menu choices as
|
|
44
53
|
* `{ "terminal:<session name>": <index into TAB_COLOUR_IDS> }`. Reading it is
|
|
45
54
|
* what makes "set the colour on the tab" register as the conversation's
|
|
46
|
-
* colour for
|
|
55
|
+
* colour - for every assistant, including one whose CLI has a colour concept
|
|
56
|
+
* of its own. */
|
|
47
57
|
export function readUserSetTabColour(terminalName) {
|
|
48
58
|
var _a;
|
|
49
59
|
try {
|
|
@@ -106,10 +116,21 @@ export function sessionForCwds(cwds, sessions) {
|
|
|
106
116
|
* colours survive a reload and one assistant's colours can never be read or
|
|
107
117
|
* written through another's routes. Reads are cached in memory and refreshed
|
|
108
118
|
* with the session poll; writes go straight through and update the cache.
|
|
119
|
+
*
|
|
120
|
+
* Every request this store makes is queued behind the last - see `_run`. That
|
|
121
|
+
* one rule is what keeps the class small: with only one of its requests ever on
|
|
122
|
+
* the wire, no answer can be older than something the cache has already taken,
|
|
123
|
+
* so none of the ordering questions this class used to carry can be asked.
|
|
109
124
|
*/
|
|
110
125
|
export class ColourStore {
|
|
111
126
|
constructor(providerId, serverSettings) {
|
|
112
127
|
this._colours = new Map();
|
|
128
|
+
this._overrides = new Set();
|
|
129
|
+
/** Unanswered writes per id, counted from the call. Read by the release
|
|
130
|
+
* affordance, which must not offer a colour still being written. */
|
|
131
|
+
this._pending = new Map();
|
|
132
|
+
/** Tail of this store's request queue - see `_run`. Never rejects. */
|
|
133
|
+
this._chain = Promise.resolve();
|
|
113
134
|
this._providerId = providerId;
|
|
114
135
|
this._serverSettings = serverSettings;
|
|
115
136
|
}
|
|
@@ -121,70 +142,217 @@ export class ColourStore {
|
|
|
121
142
|
}
|
|
122
143
|
return (_a = this._colours.get(sessionId)) !== null && _a !== void 0 ? _a : null;
|
|
123
144
|
}
|
|
145
|
+
/** Whether this conversation's colour is the user's own choice rather than
|
|
146
|
+
* one it inherited at fork time. Only a hand-set colour is offered for
|
|
147
|
+
* release: dropping an inherited one would un-inherit the branch. */
|
|
148
|
+
isOverride(sessionId) {
|
|
149
|
+
return !!sessionId && this._overrides.has(sessionId);
|
|
150
|
+
}
|
|
151
|
+
/** Whether a write against this conversation is still unanswered - queued
|
|
152
|
+
* behind an earlier request of this store's, or on the wire.
|
|
153
|
+
*
|
|
154
|
+
* Read by the release affordance - see the filter in `_openContextMenu` for
|
|
155
|
+
* why it must not offer a colour still being written. Counted from the CALL
|
|
156
|
+
* rather than from the moment the request goes out: with the store
|
|
157
|
+
* serialised (DEF-30) a capture can sit in the queue before it is sent, and a
|
|
158
|
+
* release offered in that window is undone by the capture landing after it.
|
|
159
|
+
*
|
|
160
|
+
* Not consulted by the capture gate - the cache holds only confirmed values,
|
|
161
|
+
* so reading it is already the confirmation. */
|
|
162
|
+
isPending(sessionId) {
|
|
163
|
+
var _a;
|
|
164
|
+
return !!sessionId && ((_a = this._pending.get(sessionId)) !== null && _a !== void 0 ? _a : 0) > 0;
|
|
165
|
+
}
|
|
124
166
|
/** Reload the whole store. Failures leave the previous cache in place - a
|
|
125
|
-
* momentarily unreachable server must not clear every user colour.
|
|
167
|
+
* momentarily unreachable server must not clear every user colour.
|
|
168
|
+
*
|
|
169
|
+
* The answer is taken whole. It is the only answer whose snapshot the caller
|
|
170
|
+
* ordered, and nothing this store wrote can be missing from it: the GET is
|
|
171
|
+
* not sent until every earlier write of this store's has been answered. */
|
|
126
172
|
async load() {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
173
|
+
return this._run(async () => {
|
|
174
|
+
var _a, _b;
|
|
175
|
+
try {
|
|
176
|
+
const data = await requestProvider(this._providerId, 'colours', this._serverSettings, { cache: 'no-store' });
|
|
177
|
+
const next = new Map();
|
|
178
|
+
for (const [id, colour] of Object.entries((_a = data.colours) !== null && _a !== void 0 ? _a : {})) {
|
|
179
|
+
if (typeof colour === 'string' && colour) {
|
|
180
|
+
next.set(id, colour);
|
|
181
|
+
}
|
|
134
182
|
}
|
|
183
|
+
// A store with no hand-set ids named predates the distinction, and
|
|
184
|
+
// nothing in it counts as hand-set - `colour_store._read` carries the
|
|
185
|
+
// asymmetry argument for that choice.
|
|
186
|
+
this._overrides = new Set(((_b = data.overrides) !== null && _b !== void 0 ? _b : []).filter(id => next.has(id)));
|
|
187
|
+
this._colours = next;
|
|
188
|
+
}
|
|
189
|
+
catch (_err) {
|
|
190
|
+
// Keep the cache; the next poll retries.
|
|
135
191
|
}
|
|
136
|
-
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/** Queue one request behind every request this store has already issued, and
|
|
195
|
+
* answer what it answers.
|
|
196
|
+
*
|
|
197
|
+
* This is the store's entire ordering discipline (DEF-30). Only one of its
|
|
198
|
+
* requests is ever on the wire, so an answer can never describe a moment
|
|
199
|
+
* older than one the cache has already taken. The monotonic write counter,
|
|
200
|
+
* the per-id record of the latest write and the reload generation that used
|
|
201
|
+
* to live here could only ever describe interleavings that are now
|
|
202
|
+
* unconstructible.
|
|
203
|
+
*
|
|
204
|
+
* A failed request must not poison the queue - one refused write stopping
|
|
205
|
+
* every later load, capture and release for the rest of the session is the
|
|
206
|
+
* same defect this change removes, relocated. Two things hold that, and only
|
|
207
|
+
* the first is reachable by test: each op catches its own failure and answers
|
|
208
|
+
* false, so no link rejects today; and the tail waits for SETTLEMENT rather
|
|
209
|
+
* than success, so `_chain` still cannot reject if a later edit puts throwing
|
|
210
|
+
* code outside an op's own `try` - `forget`'s filter already sits there.
|
|
211
|
+
*
|
|
212
|
+
* `requestAPI` bounds every request at `REQUEST_TIMEOUT_MS` (DEF-72), so a
|
|
213
|
+
* queued op waits a bounded time even against a server that never answers.
|
|
214
|
+
* The ceiling is per request, so K ops queued behind a dead server wait K
|
|
215
|
+
* times it, not it.
|
|
216
|
+
*
|
|
217
|
+
* An op must never call `load`, `set` or `forget` - it would wait on a chain
|
|
218
|
+
* that is waiting on it. `inherit` delegates to `set` from OUTSIDE the queue,
|
|
219
|
+
* which is why it is not itself queued. */
|
|
220
|
+
_run(op) {
|
|
221
|
+
const result = this._chain.then(op);
|
|
222
|
+
const settled = () => undefined;
|
|
223
|
+
this._chain = result.then(settled, settled);
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
/** Open a write against one or more ids. Counted, not flagged: two writes to
|
|
227
|
+
* one id can be queued at once, and a flag would let the first to settle
|
|
228
|
+
* clear the second's guard. */
|
|
229
|
+
_openWrite(ids) {
|
|
230
|
+
var _a;
|
|
231
|
+
for (const id of ids) {
|
|
232
|
+
this._pending.set(id, ((_a = this._pending.get(id)) !== null && _a !== void 0 ? _a : 0) + 1);
|
|
137
233
|
}
|
|
138
|
-
|
|
139
|
-
|
|
234
|
+
}
|
|
235
|
+
/** Close it. */
|
|
236
|
+
_closeWrite(ids) {
|
|
237
|
+
var _a;
|
|
238
|
+
for (const id of ids) {
|
|
239
|
+
const open = ((_a = this._pending.get(id)) !== null && _a !== void 0 ? _a : 1) - 1;
|
|
240
|
+
if (open > 0) {
|
|
241
|
+
this._pending.set(id, open);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
this._pending.delete(id);
|
|
245
|
+
}
|
|
140
246
|
}
|
|
141
247
|
}
|
|
142
248
|
/** Record a colour as this conversation's own. Passing null drops the entry,
|
|
143
|
-
* which is also how a deleted conversation leaves no orphan key behind.
|
|
144
|
-
|
|
249
|
+
* which is also how a deleted conversation leaves no orphan key behind.
|
|
250
|
+
* `handSet` false marks it inherited - written on a fork's behalf rather
|
|
251
|
+
* than chosen on its tab.
|
|
252
|
+
*
|
|
253
|
+
* Answers whether the store now HOLDS the colour, which is what
|
|
254
|
+
* `reconcileColours` gates its paint on.
|
|
255
|
+
*
|
|
256
|
+
* The cache is written only once the server has confirmed. Writing it
|
|
257
|
+
* eagerly and undoing that on failure looks cheaper and is not: nothing here
|
|
258
|
+
* reads the cache before this promise settles, while an unconfirmed entry is
|
|
259
|
+
* indistinguishable from a stored one to every reader - which is exactly the
|
|
260
|
+
* mistake the gate above exists to catch.
|
|
261
|
+
*
|
|
262
|
+
* "Did not throw" is not the test - the answer carries the whole store, so
|
|
263
|
+
* it is checked for the value that was just written. A server that accepted
|
|
264
|
+
* the request and then failed to persist it would otherwise be believed. */
|
|
265
|
+
async set(sessionId, colour, handSet = true) {
|
|
145
266
|
if (!sessionId) {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
if (colour) {
|
|
149
|
-
this._colours.set(sessionId, colour);
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
this._colours.delete(sessionId);
|
|
153
|
-
}
|
|
154
|
-
try {
|
|
155
|
-
await requestProvider(this._providerId, 'colours', this._serverSettings, {
|
|
156
|
-
method: 'POST',
|
|
157
|
-
body: JSON.stringify({ session_id: sessionId, colour })
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
catch (_err) {
|
|
161
|
-
// The cache already reflects the intent; the next load reconciles.
|
|
267
|
+
return false;
|
|
162
268
|
}
|
|
269
|
+
const ids = [sessionId];
|
|
270
|
+
// Marked before the op is queued, not inside it - the queue window is part
|
|
271
|
+
// of the window in which no release may be offered. See `isPending`.
|
|
272
|
+
this._openWrite(ids);
|
|
273
|
+
return this._run(async () => {
|
|
274
|
+
var _a;
|
|
275
|
+
try {
|
|
276
|
+
const data = await requestProvider(this._providerId, 'colours', this._serverSettings, {
|
|
277
|
+
method: 'POST',
|
|
278
|
+
body: JSON.stringify({
|
|
279
|
+
session_id: sessionId,
|
|
280
|
+
colour,
|
|
281
|
+
hand_set: handSet
|
|
282
|
+
})
|
|
283
|
+
});
|
|
284
|
+
if (((_a = data.colours) !== null && _a !== void 0 ? _a : {})[sessionId] !== (colour !== null && colour !== void 0 ? colour : undefined)) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
// Only this conversation's entry is taken from the answer. The rest of
|
|
288
|
+
// the map is a snapshot of a moment this caller did not order, and a
|
|
289
|
+
// reload is how the caller orders one.
|
|
290
|
+
if (colour) {
|
|
291
|
+
this._colours.set(sessionId, colour);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
this._colours.delete(sessionId);
|
|
295
|
+
}
|
|
296
|
+
this._overrides.delete(sessionId);
|
|
297
|
+
if (colour && handSet) {
|
|
298
|
+
this._overrides.add(sessionId);
|
|
299
|
+
}
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
catch (_err) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
finally {
|
|
306
|
+
this._closeWrite(ids);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
163
309
|
}
|
|
164
|
-
/** Drop stored colours
|
|
310
|
+
/** Drop stored colours - conversations that no longer exist, or overrides
|
|
311
|
+
* the user released. One request for the whole list, so a release is never
|
|
312
|
+
* left half applied. Same contract as `set`: the answer is checked for the
|
|
313
|
+
* absence of every id, and the cache is touched only once it agrees. */
|
|
165
314
|
async forget(sessionIds) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
315
|
+
return this._run(async () => {
|
|
316
|
+
// Filtered INSIDE the op, not at the call: a capture queued ahead of this
|
|
317
|
+
// release has landed by the time this runs, so the cache is the current
|
|
318
|
+
// answer to "does the store hold one". `isPending` still counts, but only
|
|
319
|
+
// for a write queued BEHIND this release - that id is in no cache yet and
|
|
320
|
+
// would otherwise be left out of a request that is going out anyway.
|
|
321
|
+
const present = sessionIds.filter(id => this._colours.has(id) || this.isPending(id));
|
|
322
|
+
if (present.length === 0) {
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
this._openWrite(present);
|
|
326
|
+
try {
|
|
327
|
+
const data = await requestProvider(this._providerId, 'colours', this._serverSettings, {
|
|
328
|
+
method: 'DELETE',
|
|
329
|
+
body: JSON.stringify({ session_ids: present })
|
|
330
|
+
});
|
|
331
|
+
if (present.some(id => { var _a; return ((_a = data.colours) !== null && _a !== void 0 ? _a : {})[id] !== undefined; })) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
for (const id of present) {
|
|
335
|
+
this._colours.delete(id);
|
|
336
|
+
this._overrides.delete(id);
|
|
337
|
+
}
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
catch (_err) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
finally {
|
|
344
|
+
this._closeWrite(present);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
180
347
|
}
|
|
181
348
|
/** Copy the parent's EFFECTIVE colour onto a fresh branch, so a branch of a
|
|
182
349
|
* branch inherits whatever its immediate parent actually shows, user-set
|
|
183
|
-
* override included. Called at the fork sites, once the new id is known.
|
|
350
|
+
* override included. Called at the fork sites, once the new id is known.
|
|
351
|
+
*/
|
|
184
352
|
async inherit(parentColour, childId) {
|
|
185
353
|
if (!parentColour || !childId) {
|
|
186
354
|
return;
|
|
187
355
|
}
|
|
188
|
-
await this.set(childId, parentColour);
|
|
356
|
+
await this.set(childId, parentColour, false);
|
|
189
357
|
}
|
|
190
358
|
}
|
package/lib/core/icons.js
CHANGED
|
@@ -40,8 +40,14 @@ export const removeIcon = new LabIcon({
|
|
|
40
40
|
// Marks the unsafe launch variants. A shield reads as "protected", which is
|
|
41
41
|
// the inverse of what these entries do, so the caution triangle carries them
|
|
42
42
|
// instead. Material "warning" outline.
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
//
|
|
44
|
+
// 16x16 and `jp-icon-warn0`, like every other icon in these menus and unlike
|
|
45
|
+
// them in colour: authored at 13x13 in `jp-icon3` it rendered smaller than
|
|
46
|
+
// its neutral siblings AND in the identical grey, measured rgb(97,97,97) in
|
|
47
|
+
// the light theme and rgb(189,189,189) in the dark one - the one glyph whose
|
|
48
|
+
// whole job is to say "this differs" was the least conspicuous in the menu.
|
|
49
|
+
const warningSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16">
|
|
50
|
+
<path class="jp-icon-warn0" fill="#f57c00" d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
|
|
45
51
|
</svg>`;
|
|
46
52
|
export const warningIcon = new LabIcon({
|
|
47
53
|
name: `${ICON_PREFIX}:warning`,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** The ONE home for the Recent section's size bounds. `src/core/panel.ts`
|
|
2
|
+
* clamps with them, `src/index.ts` defaults with them, and
|
|
3
|
+
* `scripts/generate-schema.mjs` writes them into `schema/plugin.json`, so a
|
|
4
|
+
* change here is the only change needed.
|
|
5
|
+
*
|
|
6
|
+
* Keep this module import-free. The schema generator loads the compiled
|
|
7
|
+
* `lib/core/limits.js` under plain Node, exactly as it loads the provider
|
|
8
|
+
* barrel; a single browser-only import would break that.
|
|
9
|
+
*/
|
|
10
|
+
export declare const DEFAULT_RECENT_LIMIT = 10;
|
|
11
|
+
export declare const MIN_RECENT_LIMIT = 1;
|
|
12
|
+
export declare const MAX_RECENT_LIMIT = 100;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** The ONE home for the Recent section's size bounds. `src/core/panel.ts`
|
|
2
|
+
* clamps with them, `src/index.ts` defaults with them, and
|
|
3
|
+
* `scripts/generate-schema.mjs` writes them into `schema/plugin.json`, so a
|
|
4
|
+
* change here is the only change needed.
|
|
5
|
+
*
|
|
6
|
+
* Keep this module import-free. The schema generator loads the compiled
|
|
7
|
+
* `lib/core/limits.js` under plain Node, exactly as it loads the provider
|
|
8
|
+
* barrel; a single browser-only import would break that.
|
|
9
|
+
*/
|
|
10
|
+
export const DEFAULT_RECENT_LIMIT = 10;
|
|
11
|
+
export const MIN_RECENT_LIMIT = 1;
|
|
12
|
+
export const MAX_RECENT_LIMIT = 100;
|
package/lib/core/modes.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { ILaunchMode } from './types';
|
|
2
|
+
/** A resolved launch mode, with what to call it and whether to warn. */
|
|
3
|
+
export interface IResolvedLaunchMode {
|
|
4
|
+
mode: ILaunchMode;
|
|
5
|
+
/** What the menu should call it - the value, or the mode's own label. */
|
|
6
|
+
label: string;
|
|
7
|
+
/** Whether this widens what the assistant may do without asking. */
|
|
8
|
+
unsafe: boolean;
|
|
9
|
+
}
|
|
2
10
|
/**
|
|
3
11
|
* The single launch-mode token for one action, or undefined for a plain launch.
|
|
4
12
|
*
|
|
@@ -11,3 +19,14 @@ import { ILaunchMode } from './types';
|
|
|
11
19
|
* the CLI would do unasked.
|
|
12
20
|
*/
|
|
13
21
|
export declare function resolveLaunchMode(launchModes: ILaunchMode[], values: Record<string, boolean | string>, force?: string): string | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* The mode a PLAIN launch would carry right now, as a descriptor entry.
|
|
24
|
+
*
|
|
25
|
+
* The panel marks and labels its plain menu items from this. A mode left on in
|
|
26
|
+
* settings applies to the plain items too, so deciding the warning glyph from
|
|
27
|
+
* the menu's `force` argument instead puts it on the safer-looking of two
|
|
28
|
+
* entries that build an identical launch - and leaves the one the user clicks
|
|
29
|
+
* unmarked. Answers the entry rather than the token so the caller can use the
|
|
30
|
+
* assistant's own wording.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolvedLaunchModeEntry(launchModes: ILaunchMode[], values: Record<string, boolean | string>): IResolvedLaunchMode | null;
|
package/lib/core/modes.js
CHANGED
|
@@ -34,3 +34,39 @@ export function resolveLaunchMode(launchModes, values, force) {
|
|
|
34
34
|
}
|
|
35
35
|
return undefined;
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* The mode a PLAIN launch would carry right now, as a descriptor entry.
|
|
39
|
+
*
|
|
40
|
+
* The panel marks and labels its plain menu items from this. A mode left on in
|
|
41
|
+
* settings applies to the plain items too, so deciding the warning glyph from
|
|
42
|
+
* the menu's `force` argument instead puts it on the safer-looking of two
|
|
43
|
+
* entries that build an identical launch - and leaves the one the user clicks
|
|
44
|
+
* unmarked. Answers the entry rather than the token so the caller can use the
|
|
45
|
+
* assistant's own wording.
|
|
46
|
+
*/
|
|
47
|
+
export function resolvedLaunchModeEntry(launchModes, values) {
|
|
48
|
+
var _a, _b;
|
|
49
|
+
const token = resolveLaunchMode(launchModes, values);
|
|
50
|
+
if (!token) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const [id, value] = token.split('=');
|
|
54
|
+
const mode = launchModes.find(m => m.id === id);
|
|
55
|
+
if (!mode) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
// A boolean mode with a menu label IS the assistant's skip-approval switch;
|
|
59
|
+
// an enum only widens on the values its provider declared. `plan` resolves
|
|
60
|
+
// to a mode and is stricter than the default, so "resolved at all" is the
|
|
61
|
+
// wrong question to ask about danger.
|
|
62
|
+
const unsafe = value
|
|
63
|
+
? ((_a = mode.unsafeValues) !== null && _a !== void 0 ? _a : []).indexOf(value) !== -1
|
|
64
|
+
: !!mode.menuLabel;
|
|
65
|
+
// Name the VALUE where there is one - three enum values sharing the mode's
|
|
66
|
+
// title render as one indistinguishable label.
|
|
67
|
+
return {
|
|
68
|
+
mode,
|
|
69
|
+
label: (_b = value !== null && value !== void 0 ? value : mode.menuLabel) !== null && _b !== void 0 ? _b : mode.title,
|
|
70
|
+
unsafe
|
|
71
|
+
};
|
|
72
|
+
}
|