jupyterlab_ai_code_assistants_extension 0.6.15
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/LICENSE +29 -0
- package/README.md +44 -0
- package/lib/core/colour.d.ts +57 -0
- package/lib/core/colour.js +190 -0
- package/lib/core/icons.d.ts +12 -0
- package/lib/core/icons.js +99 -0
- package/lib/core/modes.d.ts +13 -0
- package/lib/core/modes.js +36 -0
- package/lib/core/panel.d.ts +233 -0
- package/lib/core/panel.js +1673 -0
- package/lib/core/popup.d.ts +40 -0
- package/lib/core/popup.js +404 -0
- package/lib/core/registry.d.ts +24 -0
- package/lib/core/registry.js +48 -0
- package/lib/core/request.d.ts +25 -0
- package/lib/core/request.js +64 -0
- package/lib/core/terminals.d.ts +129 -0
- package/lib/core/terminals.js +344 -0
- package/lib/core/types.d.ts +269 -0
- package/lib/core/types.js +6 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +277 -0
- package/lib/providers/claude.d.ts +3 -0
- package/lib/providers/claude.js +88 -0
- package/lib/providers/codex.d.ts +3 -0
- package/lib/providers/codex.js +79 -0
- package/lib/providers/gemini.d.ts +2 -0
- package/lib/providers/gemini.js +73 -0
- package/lib/providers/index.d.ts +3 -0
- package/lib/providers/index.js +16 -0
- package/lib/providers/kimi.d.ts +23 -0
- package/lib/providers/kimi.js +139 -0
- package/package.json +169 -0
- package/schema/plugin.json +93 -0
- package/src/__tests__/colour.spec.ts +153 -0
- package/src/__tests__/core-neutrality.spec.ts +86 -0
- package/src/__tests__/labels.spec.ts +89 -0
- package/src/__tests__/launch-mode.spec.ts +53 -0
- package/src/__tests__/registry.spec.ts +178 -0
- package/src/core/colour.ts +224 -0
- package/src/core/icons.ts +121 -0
- package/src/core/modes.ts +45 -0
- package/src/core/panel.ts +2020 -0
- package/src/core/popup.ts +475 -0
- package/src/core/registry.ts +60 -0
- package/src/core/request.ts +96 -0
- package/src/core/terminals.ts +446 -0
- package/src/core/types.ts +305 -0
- package/src/index.ts +356 -0
- package/src/providers/claude.ts +102 -0
- package/src/providers/codex.ts +89 -0
- package/src/providers/gemini.ts +80 -0
- package/src/providers/index.ts +21 -0
- package/src/providers/kimi.ts +155 -0
- package/style/base.css +888 -0
- package/style/index.css +1 -0
- package/style/index.js +1 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { JupyterFrontEnd } from '@jupyterlab/application';
|
|
2
|
+
import { Dialog } from '@jupyterlab/apputils';
|
|
3
|
+
import { ServerConnection } from '@jupyterlab/services';
|
|
4
|
+
import { ITerminalTracker } from '@jupyterlab/terminal';
|
|
5
|
+
import { IColourfulTabs } from 'jupyterlab_colourful_tab_extension';
|
|
6
|
+
import { ColourStore } from './colour';
|
|
7
|
+
import { ILaunchRequest, IProviderDescriptor, ISession, ITerminalProbeResponse } from './types';
|
|
8
|
+
/** Show a modal spinner while a terminal is being launched. The caller
|
|
9
|
+
* dismisses it with `.dispose()` when the launch settles.
|
|
10
|
+
*
|
|
11
|
+
* Cancel does not abort the request - it only takes the app-wide scrim away, so
|
|
12
|
+
* a stalled launch cannot leave the user staring at a modal with no exit. If
|
|
13
|
+
* the launch later succeeds the terminal simply appears. */
|
|
14
|
+
export declare function showLaunchSpinner(title: string): Dialog<unknown>;
|
|
15
|
+
/**
|
|
16
|
+
* Terminals for one provider: the reuse ladder, the launch call and the tab
|
|
17
|
+
* tint pass.
|
|
18
|
+
*
|
|
19
|
+
* Split out of the panel because none of it is presentation - it is process
|
|
20
|
+
* identity. The panel asks for a conversation and gets a focused terminal
|
|
21
|
+
* running exactly that conversation, or a fresh one; nothing in here knows
|
|
22
|
+
* about sections, filters or menus.
|
|
23
|
+
*/
|
|
24
|
+
export declare class TerminalManager {
|
|
25
|
+
constructor(options: TerminalManager.IOptions);
|
|
26
|
+
dispose(): void;
|
|
27
|
+
/**
|
|
28
|
+
* Open a conversation, reusing an open terminal only when that terminal is
|
|
29
|
+
* POSITIVELY running it.
|
|
30
|
+
*
|
|
31
|
+
* Three steps, cheapest first: the per-project microcache, then a walk of
|
|
32
|
+
* every terminal JupyterLab knows about (each probed server-side for the
|
|
33
|
+
* conversation its process actually holds), then a fresh launch. A terminal
|
|
34
|
+
* whose conversation cannot be read is never reused - it may be running a
|
|
35
|
+
* different conversation of the same project, and a session id is globally
|
|
36
|
+
* unique, so an unreadable terminal can never be proven to be the wanted one.
|
|
37
|
+
*
|
|
38
|
+
* Concurrent clicks on the SAME conversation attach to the in-flight
|
|
39
|
+
* promise; a different conversation of the same project launches
|
|
40
|
+
* independently, so branches open side by side.
|
|
41
|
+
*/
|
|
42
|
+
openSession(session: ISession, mode: string | undefined, sessionId?: string): Promise<void>;
|
|
43
|
+
private _doOpenSession;
|
|
44
|
+
/**
|
|
45
|
+
* Launch a terminal for one request and focus it. The returned widget is
|
|
46
|
+
* tagged with `tagSessionId` in the microcache, so a later click on the row
|
|
47
|
+
* that conversation now belongs to reuses this very terminal.
|
|
48
|
+
*/
|
|
49
|
+
launch(request: ILaunchRequest, tagSessionId?: string): Promise<any>;
|
|
50
|
+
/**
|
|
51
|
+
* Bring a terminal tab to the front AND hand it keyboard focus, so typing
|
|
52
|
+
* starts without a second click. `activateById` only reveals the tab; the
|
|
53
|
+
* xterm inside does not reliably take DOM focus when the click came from the
|
|
54
|
+
* sidebar, so the focus call is deferred to the next frame - by then the
|
|
55
|
+
* widget is attached and visible.
|
|
56
|
+
*/
|
|
57
|
+
focus(widget: any): void;
|
|
58
|
+
/** Find an open terminal running exactly this conversation, or null.
|
|
59
|
+
*
|
|
60
|
+
* Matches on the conversation id ALONE. The server reads the true id from
|
|
61
|
+
* the running process, so a session the user started by hand is recognised
|
|
62
|
+
* too, not only launches that carry an argv id. An id is globally unique to
|
|
63
|
+
* one conversation, so a terminal holding it IS the one to focus, whatever
|
|
64
|
+
* cwd it reports - a process that changed directory, or whose project dir
|
|
65
|
+
* was recreated, must still be reused rather than duplicated. */
|
|
66
|
+
findForSession(wantedSessionId: string | undefined): Promise<{
|
|
67
|
+
widget: any;
|
|
68
|
+
runningId: string | null;
|
|
69
|
+
} | null>;
|
|
70
|
+
/** Ask the server what a terminal is running. Null when the widget has no
|
|
71
|
+
* session name yet, or the probe fails - a terminal can vanish between
|
|
72
|
+
* enumeration and probe, which is a retry, not an error. */
|
|
73
|
+
probe(widget: any): Promise<ITerminalProbeResponse | null>;
|
|
74
|
+
/** Turn tinting on or off. Off drops the tint from this provider's terminals
|
|
75
|
+
* at once rather than at the next reload; on re-tints from current state. */
|
|
76
|
+
setColouredTabs(on: boolean): void;
|
|
77
|
+
/** Remember the sessions the last poll returned, so the colour loop can run
|
|
78
|
+
* on its own timer without asking the panel for state. */
|
|
79
|
+
setSessions(sessions: ISession[]): void;
|
|
80
|
+
/** Arm the colour loop. Independent of panel visibility: a terminal running
|
|
81
|
+
* the assistant is tinted whether or not the panel is on screen. */
|
|
82
|
+
startColourLoop(): void;
|
|
83
|
+
stopColourLoop(): void;
|
|
84
|
+
/**
|
|
85
|
+
* Re-tint every open terminal of this assistant from the freshest colours,
|
|
86
|
+
* whether this panel launched it or the user opened it themselves.
|
|
87
|
+
*
|
|
88
|
+
* Each terminal's conversation is re-resolved on every pass rather than
|
|
89
|
+
* taken from the launch cache, which pins the launch-time conversation and
|
|
90
|
+
* goes stale on an in-place switch. On the way through, a colour the user
|
|
91
|
+
* set by hand on the tab is written back into this provider's colour store,
|
|
92
|
+
* which is what makes "set the tab colour" mean "this conversation's colour"
|
|
93
|
+
* even for an assistant whose CLI has no colour concept.
|
|
94
|
+
*/
|
|
95
|
+
reconcileColours(): Promise<void>;
|
|
96
|
+
/** Effective colour of one conversation right now, for branch inheritance
|
|
97
|
+
* at the fork sites. */
|
|
98
|
+
colourFor(session: ISession, sessionId?: string): string | null;
|
|
99
|
+
/** Drop the tint from this assistant's terminals. Only its own terminals are
|
|
100
|
+
* touched, so a tint the user set by hand on another tab survives. */
|
|
101
|
+
clearColours(): Promise<void>;
|
|
102
|
+
private _applyColour;
|
|
103
|
+
private _eachAssistantTerminal;
|
|
104
|
+
private _liveTerminals;
|
|
105
|
+
private _wireDisposal;
|
|
106
|
+
private _key;
|
|
107
|
+
private readonly _app;
|
|
108
|
+
private readonly _descriptor;
|
|
109
|
+
private readonly _colours;
|
|
110
|
+
private readonly _tracker;
|
|
111
|
+
private readonly _colourfulTabs;
|
|
112
|
+
private readonly _serverSettings;
|
|
113
|
+
private readonly _byProject;
|
|
114
|
+
private readonly _pending;
|
|
115
|
+
private _sessions;
|
|
116
|
+
private _colouredTabs;
|
|
117
|
+
private _colourHandle;
|
|
118
|
+
private _disposed;
|
|
119
|
+
}
|
|
120
|
+
export declare namespace TerminalManager {
|
|
121
|
+
interface IOptions {
|
|
122
|
+
app: JupyterFrontEnd;
|
|
123
|
+
descriptor: IProviderDescriptor;
|
|
124
|
+
colourStore: ColourStore;
|
|
125
|
+
terminalTracker: ITerminalTracker | null;
|
|
126
|
+
colourfulTabs: IColourfulTabs | null;
|
|
127
|
+
serverSettings: ServerConnection.ISettings;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { Dialog } from '@jupyterlab/apputils';
|
|
2
|
+
import { Widget } from '@lumino/widgets';
|
|
3
|
+
import { effectiveColour, readUserSetTabColour, sessionForCwds } from './colour';
|
|
4
|
+
import { requestProvider } from './request';
|
|
5
|
+
/** Colour reconciliation cadence. Matches the row poll so the two collapse
|
|
6
|
+
* into one rhythm while the panel is visible, but it is a separate timer: a
|
|
7
|
+
* terminal running the assistant is tinted whether or not the panel is open. */
|
|
8
|
+
const COLOUR_INTERVAL_MS = 30000;
|
|
9
|
+
/** Show a modal spinner while a terminal is being launched. The caller
|
|
10
|
+
* dismisses it with `.dispose()` when the launch settles.
|
|
11
|
+
*
|
|
12
|
+
* Cancel does not abort the request - it only takes the app-wide scrim away, so
|
|
13
|
+
* a stalled launch cannot leave the user staring at a modal with no exit. If
|
|
14
|
+
* the launch later succeeds the terminal simply appears. */
|
|
15
|
+
export function showLaunchSpinner(title) {
|
|
16
|
+
const body = new Widget();
|
|
17
|
+
body.node.className = 'jp-AiAssistantsPanel-launchOverlay';
|
|
18
|
+
const spinner = document.createElement('div');
|
|
19
|
+
spinner.className =
|
|
20
|
+
'jp-aiAssistants-spinner jp-AiAssistantsPanel-launchSpinner';
|
|
21
|
+
body.node.appendChild(spinner);
|
|
22
|
+
const dialog = new Dialog({
|
|
23
|
+
title,
|
|
24
|
+
body,
|
|
25
|
+
buttons: [Dialog.cancelButton()]
|
|
26
|
+
});
|
|
27
|
+
// `launch()` returns a promise nobody awaits - disposing an open Lumino
|
|
28
|
+
// dialog rejects it with `undefined`, so swallow that here and keep a benign
|
|
29
|
+
// teardown from surfacing as an unhandled rejection.
|
|
30
|
+
dialog.launch().catch(() => undefined);
|
|
31
|
+
return dialog;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Terminals for one provider: the reuse ladder, the launch call and the tab
|
|
35
|
+
* tint pass.
|
|
36
|
+
*
|
|
37
|
+
* Split out of the panel because none of it is presentation - it is process
|
|
38
|
+
* identity. The panel asks for a conversation and gets a focused terminal
|
|
39
|
+
* running exactly that conversation, or a fresh one; nothing in here knows
|
|
40
|
+
* about sections, filters or menus.
|
|
41
|
+
*/
|
|
42
|
+
export class TerminalManager {
|
|
43
|
+
constructor(options) {
|
|
44
|
+
// Most recent terminal per project, tagged with the conversation it runs so
|
|
45
|
+
// reuse can tell a project's branches apart.
|
|
46
|
+
this._byProject = new Map();
|
|
47
|
+
// In-flight launches keyed per CONVERSATION, so two branches of one project
|
|
48
|
+
// open independently and concurrently.
|
|
49
|
+
this._pending = new Map();
|
|
50
|
+
this._sessions = [];
|
|
51
|
+
this._colouredTabs = true;
|
|
52
|
+
this._colourHandle = null;
|
|
53
|
+
this._disposed = false;
|
|
54
|
+
this._app = options.app;
|
|
55
|
+
this._descriptor = options.descriptor;
|
|
56
|
+
this._colours = options.colourStore;
|
|
57
|
+
this._tracker = options.terminalTracker;
|
|
58
|
+
this._colourfulTabs = options.colourfulTabs;
|
|
59
|
+
this._serverSettings = options.serverSettings;
|
|
60
|
+
}
|
|
61
|
+
dispose() {
|
|
62
|
+
this._disposed = true;
|
|
63
|
+
this.stopColourLoop();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Open a conversation, reusing an open terminal only when that terminal is
|
|
67
|
+
* POSITIVELY running it.
|
|
68
|
+
*
|
|
69
|
+
* Three steps, cheapest first: the per-project microcache, then a walk of
|
|
70
|
+
* every terminal JupyterLab knows about (each probed server-side for the
|
|
71
|
+
* conversation its process actually holds), then a fresh launch. A terminal
|
|
72
|
+
* whose conversation cannot be read is never reused - it may be running a
|
|
73
|
+
* different conversation of the same project, and a session id is globally
|
|
74
|
+
* unique, so an unreadable terminal can never be proven to be the wanted one.
|
|
75
|
+
*
|
|
76
|
+
* Concurrent clicks on the SAME conversation attach to the in-flight
|
|
77
|
+
* promise; a different conversation of the same project launches
|
|
78
|
+
* independently, so branches open side by side.
|
|
79
|
+
*/
|
|
80
|
+
async openSession(session, mode, sessionId) {
|
|
81
|
+
const wanted = sessionId !== null && sessionId !== void 0 ? sessionId : session.session_id;
|
|
82
|
+
const key = this._key(session.project_path, wanted);
|
|
83
|
+
const inFlight = this._pending.get(key);
|
|
84
|
+
if (inFlight) {
|
|
85
|
+
return inFlight;
|
|
86
|
+
}
|
|
87
|
+
const promise = this._doOpenSession(session, mode, wanted).finally(() => {
|
|
88
|
+
this._pending.delete(key);
|
|
89
|
+
});
|
|
90
|
+
this._pending.set(key, promise);
|
|
91
|
+
return promise;
|
|
92
|
+
}
|
|
93
|
+
async _doOpenSession(session, mode, wanted) {
|
|
94
|
+
var _a;
|
|
95
|
+
// 1. Microcache - the most recent terminal of this project, reused only
|
|
96
|
+
// when it carries the wanted conversation.
|
|
97
|
+
const cached = this._byProject.get(session.project_path);
|
|
98
|
+
if (cached && !cached.widget.isDisposed && cached.sessionId === wanted) {
|
|
99
|
+
this.focus(cached.widget);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// 2. Walk every live terminal, asking the server what each is running.
|
|
103
|
+
const found = await this.findForSession(wanted);
|
|
104
|
+
if (found) {
|
|
105
|
+
// Tag with the OBSERVED conversation, so a later reuse trusts a
|
|
106
|
+
// confirmed identity rather than a wish.
|
|
107
|
+
this._byProject.set(session.project_path, {
|
|
108
|
+
widget: found.widget,
|
|
109
|
+
sessionId: (_a = found.runningId) !== null && _a !== void 0 ? _a : undefined
|
|
110
|
+
});
|
|
111
|
+
this._wireDisposal(session.project_path, found.widget);
|
|
112
|
+
this.focus(found.widget);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// 3. Fresh launch. The server picks the verb (resume, attach, bare) at
|
|
116
|
+
// launch time, because ownership of a conversation can change inside the
|
|
117
|
+
// poll window and only the server can be authoritative about it.
|
|
118
|
+
await this.launch({
|
|
119
|
+
project_path: session.project_path,
|
|
120
|
+
encoded_path: session.encoded_path,
|
|
121
|
+
session_id: wanted,
|
|
122
|
+
mode
|
|
123
|
+
}, wanted);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Launch a terminal for one request and focus it. The returned widget is
|
|
127
|
+
* tagged with `tagSessionId` in the microcache, so a later click on the row
|
|
128
|
+
* that conversation now belongs to reuses this very terminal.
|
|
129
|
+
*/
|
|
130
|
+
async launch(request, tagSessionId) {
|
|
131
|
+
const spinner = showLaunchSpinner(`Opening ${this._descriptor.label}`);
|
|
132
|
+
try {
|
|
133
|
+
const launched = await requestProvider(this._descriptor.id, 'launch', this._serverSettings, { method: 'POST', body: JSON.stringify(request) });
|
|
134
|
+
const widget = await this._app.commands.execute('terminal:open', {
|
|
135
|
+
name: launched.terminal_name
|
|
136
|
+
});
|
|
137
|
+
if (widget === null || widget === void 0 ? void 0 : widget.id) {
|
|
138
|
+
this._byProject.set(request.project_path, {
|
|
139
|
+
widget,
|
|
140
|
+
sessionId: tagSessionId
|
|
141
|
+
});
|
|
142
|
+
this._wireDisposal(request.project_path, widget);
|
|
143
|
+
this.focus(widget);
|
|
144
|
+
}
|
|
145
|
+
return widget !== null && widget !== void 0 ? widget : null;
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
spinner.dispose();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Bring a terminal tab to the front AND hand it keyboard focus, so typing
|
|
153
|
+
* starts without a second click. `activateById` only reveals the tab; the
|
|
154
|
+
* xterm inside does not reliably take DOM focus when the click came from the
|
|
155
|
+
* sidebar, so the focus call is deferred to the next frame - by then the
|
|
156
|
+
* widget is attached and visible.
|
|
157
|
+
*/
|
|
158
|
+
focus(widget) {
|
|
159
|
+
if (!widget || widget.isDisposed) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
this._app.shell.activateById(widget.id);
|
|
163
|
+
requestAnimationFrame(() => {
|
|
164
|
+
var _a, _b, _c;
|
|
165
|
+
try {
|
|
166
|
+
(_c = (_b = (_a = widget.content) === null || _a === void 0 ? void 0 : _a.term) === null || _b === void 0 ? void 0 : _b.focus) === null || _c === void 0 ? void 0 : _c.call(_b);
|
|
167
|
+
}
|
|
168
|
+
catch (_err) {
|
|
169
|
+
// Disposed in the meantime - nothing to focus.
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/** Find an open terminal running exactly this conversation, or null.
|
|
174
|
+
*
|
|
175
|
+
* Matches on the conversation id ALONE. The server reads the true id from
|
|
176
|
+
* the running process, so a session the user started by hand is recognised
|
|
177
|
+
* too, not only launches that carry an argv id. An id is globally unique to
|
|
178
|
+
* one conversation, so a terminal holding it IS the one to focus, whatever
|
|
179
|
+
* cwd it reports - a process that changed directory, or whose project dir
|
|
180
|
+
* was recreated, must still be reused rather than duplicated. */
|
|
181
|
+
async findForSession(wantedSessionId) {
|
|
182
|
+
if (!this._tracker || !wantedSessionId) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
for (const widget of this._liveTerminals()) {
|
|
186
|
+
const info = await this.probe(widget);
|
|
187
|
+
if (info && info.session_id && info.session_id === wantedSessionId) {
|
|
188
|
+
return { widget, runningId: info.session_id };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
/** Ask the server what a terminal is running. Null when the widget has no
|
|
194
|
+
* session name yet, or the probe fails - a terminal can vanish between
|
|
195
|
+
* enumeration and probe, which is a retry, not an error. */
|
|
196
|
+
async probe(widget) {
|
|
197
|
+
var _a, _b;
|
|
198
|
+
const name = (_b = (_a = widget === null || widget === void 0 ? void 0 : widget.content) === null || _a === void 0 ? void 0 : _a.session) === null || _b === void 0 ? void 0 : _b.name;
|
|
199
|
+
if (typeof name !== 'string' || !name) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
return await requestProvider(this._descriptor.id, `terminal/${encodeURIComponent(name)}`, this._serverSettings);
|
|
204
|
+
}
|
|
205
|
+
catch (_err) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// --------------------------------------------------------------- colours
|
|
210
|
+
/** Turn tinting on or off. Off drops the tint from this provider's terminals
|
|
211
|
+
* at once rather than at the next reload; on re-tints from current state. */
|
|
212
|
+
setColouredTabs(on) {
|
|
213
|
+
const enabled = !!on;
|
|
214
|
+
if (this._colouredTabs === enabled) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
this._colouredTabs = enabled;
|
|
218
|
+
void (enabled ? this.reconcileColours() : this.clearColours());
|
|
219
|
+
}
|
|
220
|
+
/** Remember the sessions the last poll returned, so the colour loop can run
|
|
221
|
+
* on its own timer without asking the panel for state. */
|
|
222
|
+
setSessions(sessions) {
|
|
223
|
+
this._sessions = sessions;
|
|
224
|
+
}
|
|
225
|
+
/** Arm the colour loop. Independent of panel visibility: a terminal running
|
|
226
|
+
* the assistant is tinted whether or not the panel is on screen. */
|
|
227
|
+
startColourLoop() {
|
|
228
|
+
if (this._disposed || this._colourHandle !== null) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
this._colourHandle = window.setTimeout(() => {
|
|
232
|
+
this._colourHandle = null;
|
|
233
|
+
void this.reconcileColours();
|
|
234
|
+
}, COLOUR_INTERVAL_MS);
|
|
235
|
+
}
|
|
236
|
+
stopColourLoop() {
|
|
237
|
+
if (this._colourHandle !== null) {
|
|
238
|
+
window.clearTimeout(this._colourHandle);
|
|
239
|
+
this._colourHandle = null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Re-tint every open terminal of this assistant from the freshest colours,
|
|
244
|
+
* whether this panel launched it or the user opened it themselves.
|
|
245
|
+
*
|
|
246
|
+
* Each terminal's conversation is re-resolved on every pass rather than
|
|
247
|
+
* taken from the launch cache, which pins the launch-time conversation and
|
|
248
|
+
* goes stale on an in-place switch. On the way through, a colour the user
|
|
249
|
+
* set by hand on the tab is written back into this provider's colour store,
|
|
250
|
+
* which is what makes "set the tab colour" mean "this conversation's colour"
|
|
251
|
+
* even for an assistant whose CLI has no colour concept.
|
|
252
|
+
*/
|
|
253
|
+
async reconcileColours() {
|
|
254
|
+
if (!this._colouredTabs) {
|
|
255
|
+
// No rearm: `setColouredTabs(true)` calls straight back in here.
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
await this._eachAssistantTerminal(async (widget, info) => {
|
|
260
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
261
|
+
const sessionId = (_d = (_a = info.session_id) !== null && _a !== void 0 ? _a : (_c = sessionForCwds((_b = info.cwds) !== null && _b !== void 0 ? _b : [], this._sessions)) === null || _c === void 0 ? void 0 : _c.session_id) !== null && _d !== void 0 ? _d : null;
|
|
262
|
+
const terminalName = (_g = (_f = (_e = widget === null || widget === void 0 ? void 0 : widget.content) === null || _e === void 0 ? void 0 : _e.session) === null || _f === void 0 ? void 0 : _f.name) !== null && _g !== void 0 ? _g : '';
|
|
263
|
+
// Write-back. Native-colour assistants own their colour, so their
|
|
264
|
+
// tabs are read-only here; for the others the tab IS the control
|
|
265
|
+
// surface.
|
|
266
|
+
if (sessionId &&
|
|
267
|
+
terminalName &&
|
|
268
|
+
this._descriptor.colourSource !== 'native') {
|
|
269
|
+
const userSet = readUserSetTabColour(terminalName);
|
|
270
|
+
if (userSet && userSet !== this._colours.get(sessionId)) {
|
|
271
|
+
await this._colours.set(sessionId, userSet);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const native = (_h = info.colour) !== null && _h !== void 0 ? _h : (sessionId
|
|
275
|
+
? ((_k = (_j = this._sessions.find(s => s.session_id === sessionId)) === null || _j === void 0 ? void 0 : _j.colour) !== null && _k !== void 0 ? _k : null)
|
|
276
|
+
: null);
|
|
277
|
+
this._applyColour(widget, effectiveColour(sessionId, this._descriptor.colourSource, this._colours.get(sessionId), native));
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
// Always rearm, even after a failed pass - one bad probe must not end
|
|
282
|
+
// tinting for the rest of the session.
|
|
283
|
+
this.startColourLoop();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/** Effective colour of one conversation right now, for branch inheritance
|
|
287
|
+
* at the fork sites. */
|
|
288
|
+
colourFor(session, sessionId) {
|
|
289
|
+
var _a;
|
|
290
|
+
const id = sessionId !== null && sessionId !== void 0 ? sessionId : session.session_id;
|
|
291
|
+
return effectiveColour(id, this._descriptor.colourSource, this._colours.get(id), (_a = session.colour) !== null && _a !== void 0 ? _a : null);
|
|
292
|
+
}
|
|
293
|
+
/** Drop the tint from this assistant's terminals. Only its own terminals are
|
|
294
|
+
* touched, so a tint the user set by hand on another tab survives. */
|
|
295
|
+
async clearColours() {
|
|
296
|
+
await this._eachAssistantTerminal(async (widget) => {
|
|
297
|
+
this._applyColour(widget, null);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
_applyColour(widget, colourId) {
|
|
301
|
+
if (!this._colourfulTabs || !widget || widget.isDisposed) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
this._colourfulTabs.setColour(widget, colourId);
|
|
305
|
+
}
|
|
306
|
+
async _eachAssistantTerminal(apply) {
|
|
307
|
+
if (!this._colourfulTabs || !this._tracker) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const widgets = this._liveTerminals();
|
|
311
|
+
await Promise.all(widgets.map(async (widget) => {
|
|
312
|
+
const info = await this.probe(widget);
|
|
313
|
+
if (!info || !info.running || widget.isDisposed) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
await apply(widget, info);
|
|
317
|
+
}));
|
|
318
|
+
}
|
|
319
|
+
_liveTerminals() {
|
|
320
|
+
var _a;
|
|
321
|
+
const widgets = [];
|
|
322
|
+
(_a = this._tracker) === null || _a === void 0 ? void 0 : _a.forEach((widget) => {
|
|
323
|
+
if (widget && !widget.isDisposed) {
|
|
324
|
+
widgets.push(widget);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
return widgets;
|
|
328
|
+
}
|
|
329
|
+
_wireDisposal(projectPath, widget) {
|
|
330
|
+
var _a;
|
|
331
|
+
if (!((_a = widget === null || widget === void 0 ? void 0 : widget.disposed) === null || _a === void 0 ? void 0 : _a.connect)) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
widget.disposed.connect(() => {
|
|
335
|
+
var _a;
|
|
336
|
+
if (((_a = this._byProject.get(projectPath)) === null || _a === void 0 ? void 0 : _a.widget) === widget) {
|
|
337
|
+
this._byProject.delete(projectPath);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
_key(projectPath, sessionId) {
|
|
342
|
+
return `${projectPath}\n${sessionId !== null && sessionId !== void 0 ? sessionId : ''}`;
|
|
343
|
+
}
|
|
344
|
+
}
|