Skip to content

Commit 7b32433

Browse files
committed
Implement agent busy state
1 parent 06e4af9 commit 7b32433

4 files changed

Lines changed: 84 additions & 1 deletion

File tree

src/agent.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,20 @@ export class AgentManager {
435435
return this._tokenUsageChanged;
436436
}
437437

438+
/**
439+
* Signal emitted when the agent's busy state changes (generating response or executing tools)
440+
*/
441+
get busyChanged(): ISignal<this, boolean> {
442+
return this._busyChanged;
443+
}
444+
445+
/**
446+
* Whether the agent is currently generating a response or executing tools
447+
*/
448+
get busy(): boolean {
449+
return this._busy;
450+
}
451+
438452
/**
439453
* Refresh the skills snapshot and rebuild the agent if resources are ready.
440454
*/
@@ -592,6 +606,9 @@ export class AgentManager {
592606
* @param message The user message to respond to (may include processed attachment content)
593607
*/
594608
async generateResponse(message: string): Promise<void> {
609+
// Abort any ongoing streaming immediately
610+
this.stopStreaming();
611+
this._setBusy(true);
595612
this._controller = new AbortController();
596613

597614
try {
@@ -659,6 +676,7 @@ export class AgentManager {
659676
}
660677
} finally {
661678
this._controller = null;
679+
this._setBusy(false);
662680
}
663681
}
664682

@@ -1191,6 +1209,16 @@ WEB RETRIEVAL POLICY:
11911209
return `Supported MIME types in this session: ${safeMimeTypes.join(', ')}`;
11921210
}
11931211

1212+
/**
1213+
* Set busy state and emit signal
1214+
*/
1215+
private _setBusy(busy: boolean): void {
1216+
if (this._busy !== busy) {
1217+
this._busy = busy;
1218+
this._busyChanged.emit(busy);
1219+
}
1220+
}
1221+
11941222
// Private attributes
11951223
private _settingsModel: AISettingsModel;
11961224
private _toolRegistry?: IToolRegistry;
@@ -1201,6 +1229,8 @@ WEB RETRIEVAL POLICY:
12011229
private _agent: ToolLoopAgent<never, ToolMap> | null;
12021230
private _history: ModelMessage[];
12031231
private _mcpTools: ToolMap;
1232+
private _busy: boolean = false;
1233+
private _busyChanged = new Signal<this, boolean>(this);
12041234
private _controller: AbortController | null;
12051235
private _agentEvent: Signal<this, IAgentEvent>;
12061236
private _tokenUsage: ITokenUsage;

src/chat-model.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,24 @@ export class AIChatModel extends AbstractChatModel {
109109
this._agentManager = options.agentManager;
110110
this._trans = options.trans;
111111

112-
// Listen for agent events
112+
// Listen for agent events and busy state
113113
this._agentManager.agentEvent.connect(this._onAgentEvent, this);
114+
this._agentManager.busyChanged.connect((_, busy) => {
115+
this._busyChanged.emit(busy);
116+
});
114117

115118
// Listen for settings changes to update chat behavior
116119
this._settingsModel.stateChanged.connect(this._onSettingsChanged, this);
120+
121+
// Prevent clearing input field when agent is busy.
122+
const originalSend = this.input.send;
123+
this.input.send = (content: string) => {
124+
if (this._agentManager.busy) {
125+
return;
126+
}
127+
originalSend(content);
128+
};
129+
117130
this.setReady();
118131
}
119132

@@ -156,6 +169,20 @@ export class AIChatModel extends AbstractChatModel {
156169
return this._agentManager;
157170
}
158171

172+
/**
173+
* Whether the agent is currently busy generating a response or executing tools.
174+
*/
175+
get busy(): boolean {
176+
return this._agentManager.busy;
177+
}
178+
179+
/**
180+
* A signal emitted when the busy state changes.
181+
*/
182+
get busyChanged(): ISignal<this, boolean> {
183+
return this._busyChanged;
184+
}
185+
159186
/**
160187
* Creates a chat context for the current conversation.
161188
*/
@@ -214,6 +241,11 @@ export class AIChatModel extends AbstractChatModel {
214241
return;
215242
}
216243

244+
// Prevent sending multiple messages concurrently
245+
if (this._agentManager.busy) {
246+
return;
247+
}
248+
217249
// Add user message to chat
218250
const userMessage: IMessageContent = {
219251
body: message.body,
@@ -916,6 +948,7 @@ export class AIChatModel extends AbstractChatModel {
916948
}
917949

918950
// Private fields
951+
private _busyChanged = new Signal<this, boolean>(this);
919952
private _settingsModel: AISettingsModel;
920953
private _user: IUser;
921954
private _toolContexts: Map<string, IToolExecutionContext> = new Map();

src/widgets/main-area-chat.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ export class MainAreaChat extends MainAreaWidget<ChatWidget> {
6464
});
6565

6666
this.model.writersChanged.connect(this._writersChanged);
67+
// Toggle busy class based on agent state
68+
this.model.busyChanged.connect((_, busy) => {
69+
this.content.toggleClass('jp-ai-busy', busy);
70+
});
71+
// Set initial state
72+
this.content.toggleClass('jp-ai-busy', this.model.busy);
6773
}
6874

6975
dispose(): void {

style/base.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,3 +344,17 @@
344344
.jp-ai-completion-status .jp-ai-completion-disabled path {
345345
fill: var(--jp-layout-color2);
346346
}
347+
348+
/**
349+
* Busy state styling for locking the chat input.
350+
*/
351+
.jp-ai-busy .jp-chat-input-container {
352+
pointer-events: none;
353+
opacity: 0.6;
354+
filter: grayscale(0.5);
355+
cursor: not-allowed;
356+
}
357+
358+
.jp-ai-busy .jp-chat-input-textfield {
359+
cursor: not-allowed;
360+
}

0 commit comments

Comments
 (0)