Skip to content

Commit 484badf

Browse files
luoxuanzaoQoder-AI
andauthored
feat(chat): surface model capacity queue progress on the waiting indicator (#78)
The CLI emits model_queue_status while a request waits for capacity; the plugin maps it to a model_queue stream chunk and shows the queue position and estimated wait on the thinking indicator, restoring the usual wording once the request is ready. Older CLIs that never emit the subtype are unaffected. Co-authored-by: QoderAI (Qwen 3.8 Max) <qoder_ai@qoder.com>
1 parent 02a8c46 commit 484badf

7 files changed

Lines changed: 108 additions & 3 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ version with its date and start a fresh empty `[Unreleased]` above it.
3030
The toggle is off by default; off keeps the numbered tabs and the existing
3131
interactions unchanged.
3232

33+
- When the selected model is at capacity, the waiting indicator now says so
34+
with the queue position and estimated wait, e.g.
35+
`Model is queued (3 ahead · ~12s wait)...`, and returns to the usual
36+
wording as soon as the request leaves the queue.
37+
3338
## [1.0.14] - 2026-09-20
3439

3540
### Changed

‎src/core/types/chat.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ export type StreamChunk =
154154
| { type: 'tool_output'; id: string; content: string }
155155
| { type: 'error'; content: string; code?: string }
156156
| { type: 'notice'; content: string; level?: 'info' | 'warning' }
157+
| { type: 'model_queue'; status: 'queued' | 'ready'; queueCount?: number; waitTimeMs?: number }
157158
| { type: 'done' }
158159
| { type: 'usage'; usage: UsageInfo; sessionId?: string | null }
159160
| { type: 'context_compacted' }

‎src/features/chat/controllers/stream-controller.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ export class StreamController {
174174
await this.appendText(`\n\n⚠️ **${chunk.level === 'warning' ? 'Blocked' : 'Notice'}:** ${chunk.content}`);
175175
break;
176176

177+
case 'model_queue':
178+
this.thinkingIndicator.setQueue(chunk);
179+
break;
180+
177181
case 'error':
178182
await this.handleError(chunk, msg);
179183
break;

‎src/features/chat/controllers/thinking-indicator-controller.ts‎

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { formatDurationMmSs } from '../../../core/time/date';
2+
import type { StreamChunk } from '../../../core/types';
23
import { FLAVOR_TEXTS } from '../flavor-texts';
34
import type { ChatState } from '../state/chat-state';
45

@@ -8,10 +9,21 @@ interface ThinkingIndicatorControllerDeps {
89
updateQueueIndicator: () => void;
910
}
1011

12+
type ModelQueueChunk = Extract<StreamChunk, { type: 'model_queue' }>;
13+
1114
const SHOW_DELAY_MS = 400;
1215

16+
function buildQueueLabel(chunk: ModelQueueChunk): string {
17+
const parts: string[] = [];
18+
if (chunk.queueCount && chunk.queueCount > 0) parts.push(`${chunk.queueCount} ahead`);
19+
if (chunk.waitTimeMs && chunk.waitTimeMs > 0) parts.push(`~${Math.ceil(chunk.waitTimeMs / 1000)}s wait`);
20+
return parts.length > 0 ? `Model is queued (${parts.join(' · ')})...` : 'Model is queued...';
21+
}
22+
1323
/** Owns the delayed flavor-text indicator and its elapsed-time timer. */
1424
export class ThinkingIndicatorController {
25+
private queueLabel: string | null = null;
26+
1527
constructor(private readonly deps: ThinkingIndicatorControllerDeps) {}
1628

1729
show(overrideText?: string, overrideCls?: string): void {
@@ -35,10 +47,12 @@ export class ThinkingIndicatorController {
3547
state.setThinkingIndicatorTimeout(null, null);
3648
if (!state.currentContentEl || state.thinkingEl || state.currentThinkingState) return;
3749

38-
const cls = overrideCls ? `qoderian-thinking ${overrideCls}` : 'qoderian-thinking';
50+
const queueActive = this.queueLabel !== null;
51+
const queueCls = queueActive ? ' qoderian-thinking--queue' : '';
52+
const cls = `qoderian-thinking${overrideCls ? ` ${overrideCls}` : queueCls}`;
3953
state.thinkingEl = state.currentContentEl.createDiv({ cls });
40-
const text = overrideText || FLAVOR_TEXTS[Math.floor(Math.random() * FLAVOR_TEXTS.length)];
41-
state.thinkingEl.createSpan({ text });
54+
const text = overrideText || this.queueLabel || FLAVOR_TEXTS[Math.floor(Math.random() * FLAVOR_TEXTS.length)];
55+
state.thinkingEl.createSpan({ cls: 'qoderian-thinking-text', text });
4256

4357
const timerSpan = state.thinkingEl.createSpan({ cls: 'qoderian-thinking-hint' });
4458
const updateTimer = () => {
@@ -64,8 +78,32 @@ export class ThinkingIndicatorController {
6478
}, SHOW_DELAY_MS), timerWindow);
6579
}
6680

81+
/** Reflects CLI model-capacity queue progress onto the waiting indicator. */
82+
setQueue(chunk: ModelQueueChunk): void {
83+
const { state } = this.deps;
84+
if (chunk.status === 'ready') {
85+
if (this.queueLabel === null) return;
86+
this.queueLabel = null;
87+
if (state.thinkingEl) {
88+
state.thinkingEl.removeClass('qoderian-thinking--queue');
89+
const label = state.thinkingEl.querySelector<HTMLElement>('.qoderian-thinking-text');
90+
label?.setText(FLAVOR_TEXTS[Math.floor(Math.random() * FLAVOR_TEXTS.length)]);
91+
}
92+
return;
93+
}
94+
95+
this.queueLabel = buildQueueLabel(chunk);
96+
if (state.thinkingEl) {
97+
state.thinkingEl.addClass('qoderian-thinking--queue');
98+
state.thinkingEl.querySelector<HTMLElement>('.qoderian-thinking-text')?.setText(this.queueLabel);
99+
return;
100+
}
101+
this.show(this.queueLabel, 'qoderian-thinking--queue');
102+
}
103+
67104
hide(): void {
68105
const { state } = this.deps;
106+
this.queueLabel = null;
69107
if (state.thinkingIndicatorTimeout) {
70108
const activeWindow = this.deps.getMessagesEl().ownerDocument.defaultView ?? window;
71109
state.clearThinkingIndicatorTimeout(activeWindow);

‎src/qoder/stream/transform-qoder-message.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,13 @@ export function* transformSDKMessage(
428428
if (notification) {
429429
yield notification;
430430
}
431+
} else if (message.subtype === 'model_queue_status') {
432+
yield {
433+
type: 'model_queue',
434+
status: message.status,
435+
queueCount: message.queue_count,
436+
waitTimeMs: message.wait_time_ms,
437+
};
431438
}
432439
break;
433440

‎src/style/components/thinking.css‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
color: var(--qoderian-compact);
1111
}
1212

13+
.qoderian-thinking.qoderian-thinking--queue {
14+
color: var(--text-warning);
15+
}
16+
1317
.qoderian-thinking-hint {
1418
color: var(--text-muted);
1519
font-style: normal;

‎tests/unit/qoder/stream/transform-qoder-message.test.ts‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,52 @@ describe('transformSDKMessage', () => {
136136
},
137137
]);
138138
});
139+
140+
it('maps model_queue_status queued progress to a model_queue chunk', () => {
141+
const message = msg({
142+
type: 'system',
143+
subtype: 'model_queue_status',
144+
status: 'queued',
145+
request_id: 'req-1',
146+
request_set_id: 'set-1',
147+
model_key: 'qoder-model-queue-test',
148+
queue_count: 3,
149+
wait_time_ms: 12000,
150+
} as any);
151+
152+
const results = [...transformSDKMessage(message)];
153+
154+
expect(results).toEqual([
155+
{
156+
type: 'model_queue',
157+
status: 'queued',
158+
queueCount: 3,
159+
waitTimeMs: 12000,
160+
},
161+
]);
162+
});
163+
164+
it('maps model_queue_status ready without optional queue fields', () => {
165+
const message = msg({
166+
type: 'system',
167+
subtype: 'model_queue_status',
168+
status: 'ready',
169+
request_id: 'req-2',
170+
request_set_id: 'set-2',
171+
model_key: 'qoder-model-queue-test',
172+
} as any);
173+
174+
const results = [...transformSDKMessage(message)];
175+
176+
expect(results).toEqual([
177+
{
178+
type: 'model_queue',
179+
status: 'ready',
180+
queueCount: undefined,
181+
waitTimeMs: undefined,
182+
},
183+
]);
184+
});
139185
});
140186

141187
describe('assistant messages', () => {

0 commit comments

Comments
 (0)