Skip to content

Commit 5dbba83

Browse files
committed
feat(ai): allow typing and message queuing during AI streaming
Keep the textarea enabled while AI is responding so users can type a follow-up clarification mid-turn. Queued messages are injected into Claude's context via tool response hints and a new getUserClarification MCP tool, letting Claude incorporate feedback naturally without waiting for the turn to end. - Add queueClarification/getAndClearClarification/clearClarification node endpoints with image support - Add getUserClarification MCP tool and clarification hints on all tool responses (both hook-based and MCP) - Static queue bubble above input area with Edit button - Auto-send queued message as next turn if not consumed mid-turn - Show consumed clarification as user message bubble in chat
1 parent 15c0b74 commit 5dbba83

9 files changed

Lines changed: 489 additions & 47 deletions

File tree

docs/API-Reference/command/Commands.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -992,7 +992,7 @@ Performs a mixed reset
992992
## CMD\_GIT\_TOGGLE\_PANEL
993993
Toggles the git panel
994994

995-
**Kind**: global variable
995+
**Kind**: global variable
996996
<a name="CMD_CUSTOM_SNIPPETS_PANEL"></a>
997997

998998
## CMD\_CUSTOM\_SNIPPETS\_PANEL

docs/API-Reference/document/Document.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ Given a character index within the document text (assuming \n newlines),
248248
returns the corresponding {line, ch} position. Works whether or not
249249
a master editor is attached.
250250

251-
**Kind**: instance method of [<code>Document</code>](#Document)
251+
**Kind**: instance method of [<code>Document</code>](#Document)
252252

253253
| Param | Type | Description |
254254
| --- | --- | --- |

docs/API-Reference/view/PanelView.md

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ Sets the panel's visibility state
9393
### panel.setTitle(newTitle)
9494
Updates the display title shown in the tab bar for this panel.
9595

96-
**Kind**: instance method of [<code>Panel</code>](#Panel)
96+
**Kind**: instance method of [<code>Panel</code>](#Panel)
9797

9898
| Param | Type | Description |
9999
| --- | --- | --- |
@@ -105,7 +105,7 @@ Updates the display title shown in the tab bar for this panel.
105105
Destroys the panel, removing it from the tab bar, internal maps, and the DOM.
106106
After calling this, the Panel instance should not be reused.
107107

108-
**Kind**: instance method of [<code>Panel</code>](#Panel)
108+
**Kind**: instance method of [<code>Panel</code>](#Panel)
109109
<a name="Panel+getPanelType"></a>
110110

111111
### panel.getPanelType() ⇒ <code>string</code>
@@ -117,37 +117,61 @@ gets the Panel's type
117117
## \_panelMap : <code>Object.&lt;string, Panel&gt;</code>
118118
Maps panel ID to Panel instance
119119

120-
**Kind**: global variable
120+
**Kind**: global variable
121121
<a name="_$container"></a>
122122

123123
## \_$container : <code>jQueryObject</code>
124124
The single container wrapping all bottom panels
125125

126-
**Kind**: global variable
126+
**Kind**: global variable
127127
<a name="_$tabBar"></a>
128128

129129
## \_$tabBar : <code>jQueryObject</code>
130130
The tab bar inside the container
131131

132-
**Kind**: global variable
132+
**Kind**: global variable
133133
<a name="_$tabsOverflow"></a>
134134

135135
## \_$tabsOverflow : <code>jQueryObject</code>
136136
Scrollable area holding the tab elements
137137

138-
**Kind**: global variable
138+
**Kind**: global variable
139139
<a name="_openIds"></a>
140140

141141
## \_openIds : <code>Array.&lt;string&gt;</code>
142142
Ordered list of currently open (tabbed) panel IDs
143143

144-
**Kind**: global variable
144+
**Kind**: global variable
145145
<a name="_activeId"></a>
146146

147147
## \_activeId : <code>string</code> \| <code>null</code>
148148
The panel ID of the currently visible (active) tab
149149

150-
**Kind**: global variable
150+
**Kind**: global variable
151+
<a name="_isMaximized"></a>
152+
153+
## \_isMaximized : <code>boolean</code>
154+
Whether the bottom panel is currently maximized
155+
156+
**Kind**: global variable
157+
<a name="_preMaximizeHeight"></a>
158+
159+
## \_preMaximizeHeight : <code>number</code> \| <code>null</code>
160+
The panel height before maximize, for restore
161+
162+
**Kind**: global variable
163+
<a name="_$editorHolder"></a>
164+
165+
## \_$editorHolder : <code>jQueryObject</code>
166+
The editor holder element, passed from WorkspaceManager
167+
168+
**Kind**: global variable
169+
<a name="_recomputeLayout"></a>
170+
171+
## \_recomputeLayout : <code>function</code>
172+
recomputeLayout callback from WorkspaceManager
173+
174+
**Kind**: global variable
151175
<a name="EVENT_PANEL_HIDDEN"></a>
152176

153177
## EVENT\_PANEL\_HIDDEN : <code>string</code>
@@ -165,31 +189,80 @@ Event when panel is shown
165189
## PANEL\_TYPE\_BOTTOM\_PANEL : <code>string</code>
166190
type for bottom panel
167191

192+
**Kind**: global constant
193+
<a name="MAXIMIZE_THRESHOLD"></a>
194+
195+
## MAXIMIZE\_THRESHOLD : <code>number</code>
196+
Pixel threshold for detecting near-maximize state during resize.
197+
If the editor holder height is within this many pixels of zero, the
198+
panel is treated as maximized. Keeps the maximize icon responsive
199+
during drag without being overly sensitive.
200+
201+
**Kind**: global constant
202+
<a name="MIN_PANEL_HEIGHT"></a>
203+
204+
## MIN\_PANEL\_HEIGHT : <code>number</code>
205+
Minimum panel height (matches Resizer minSize) used as a floor
206+
when computing a sensible restore height.
207+
168208
**Kind**: global constant
169209
<a name="init"></a>
170210

171-
## init($container, $tabBar, $tabsOverflow)
211+
## init($container, $tabBar, $tabsOverflow, $editorHolder, recomputeLayoutFn)
172212
Initializes the PanelView module with references to the bottom panel container DOM elements.
173213
Called by WorkspaceManager during htmlReady.
174214

175-
**Kind**: global function
215+
**Kind**: global function
176216

177217
| Param | Type | Description |
178218
| --- | --- | --- |
179219
| $container | <code>jQueryObject</code> | The bottom panel container element. |
180220
| $tabBar | <code>jQueryObject</code> | The tab bar element inside the container. |
181221
| $tabsOverflow | <code>jQueryObject</code> | The scrollable area holding tab elements. |
222+
| $editorHolder | <code>jQueryObject</code> | The editor holder element (for maximize height calculation). |
223+
| recomputeLayoutFn | <code>function</code> | Callback to trigger workspace layout recomputation. |
224+
225+
<a name="exitMaximizeOnResize"></a>
226+
227+
## exitMaximizeOnResize()
228+
Exit maximize state without resizing (for external callers like drag-resize).
229+
Clears internal maximize state and resets the button icon.
230+
231+
**Kind**: global function
232+
<a name="enterMaximizeOnResize"></a>
233+
234+
## enterMaximizeOnResize()
235+
Enter maximize state during a drag-resize that reaches the maximum
236+
height. No pre-maximize height is stored because the user arrived
237+
here via continuous dragging; a sensible default will be computed if
238+
they later click the Restore button.
239+
240+
**Kind**: global function
241+
<a name="restoreIfMaximized"></a>
242+
243+
## restoreIfMaximized()
244+
Restore the container's CSS height to the pre-maximize value and clear maximize state.
245+
Must be called BEFORE Resizer.hide() so the Resizer reads the correct height.
246+
If not maximized, this is a no-op.
247+
When the saved height is near-max or unknown, a sensible default is used.
248+
249+
**Kind**: global function
250+
<a name="isMaximized"></a>
251+
252+
## isMaximized() ⇒ <code>boolean</code>
253+
Returns true if the bottom panel is currently maximized.
182254

255+
**Kind**: global function
183256
<a name="getOpenBottomPanelIDs"></a>
184257

185258
## getOpenBottomPanelIDs() ⇒ <code>Array.&lt;string&gt;</code>
186259
Returns a copy of the currently open bottom panel IDs in tab order.
187260

188-
**Kind**: global function
261+
**Kind**: global function
189262
<a name="hideAllOpenPanels"></a>
190263

191264
## hideAllOpenPanels() ⇒ <code>Array.&lt;string&gt;</code>
192265
Hides every open bottom panel tab in a single batch
193266

194-
**Kind**: global function
195-
**Returns**: <code>Array.&lt;string&gt;</code> - The IDs of panels that were open (useful for restoring later).
267+
**Kind**: global function
268+
**Returns**: <code>Array.&lt;string&gt;</code> - The IDs of panels that were open (useful for restoring later).

docs/API-Reference/view/WorkspaceManager.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,19 +56,19 @@ Constant representing the type of plugin panel
5656
### view/WorkspaceManager.$bottomPanelContainer : <code>jQueryObject</code>
5757
The single container wrapping all bottom panels
5858

59-
**Kind**: inner property of [<code>view/WorkspaceManager</code>](#module_view/WorkspaceManager)
59+
**Kind**: inner property of [<code>view/WorkspaceManager</code>](#module_view/WorkspaceManager)
6060
<a name="module_view/WorkspaceManager..$statusBarPanelToggle"></a>
6161

6262
### view/WorkspaceManager.$statusBarPanelToggle : <code>jQueryObject</code>
6363
Chevron toggle in the status bar
6464

65-
**Kind**: inner property of [<code>view/WorkspaceManager</code>](#module_view/WorkspaceManager)
65+
**Kind**: inner property of [<code>view/WorkspaceManager</code>](#module_view/WorkspaceManager)
6666
<a name="module_view/WorkspaceManager.._statusBarToggleInProgress"></a>
6767

6868
### view/WorkspaceManager.\_statusBarToggleInProgress : <code>boolean</code>
6969
True while the status bar toggle button is handling a click
7070

71-
**Kind**: inner property of [<code>view/WorkspaceManager</code>](#module_view/WorkspaceManager)
71+
**Kind**: inner property of [<code>view/WorkspaceManager</code>](#module_view/WorkspaceManager)
7272
<a name="module_view/WorkspaceManager..EVENT_WORKSPACE_UPDATE_LAYOUT"></a>
7373

7474
### view/WorkspaceManager.EVENT\_WORKSPACE\_UPDATE\_LAYOUT
@@ -108,7 +108,7 @@ The panel's size & visibility are automatically saved & restored as a view-state
108108
Destroys a bottom panel, removing it from internal registries, the tab bar, and the DOM.
109109
After calling this, the panel ID is no longer valid and the Panel instance should not be reused.
110110

111-
**Kind**: inner method of [<code>view/WorkspaceManager</code>](#module_view/WorkspaceManager)
111+
**Kind**: inner method of [<code>view/WorkspaceManager</code>](#module_view/WorkspaceManager)
112112

113113
| Param | Type | Description |
114114
| --- | --- | --- |

src-node/claude-code-agent.js

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ const { createEditorMcpServer } = require("./mcp-editor-tools");
3232

3333
const CONNECTOR_ID = "ph_ai_claude";
3434

35+
const CLARIFICATION_HINT =
36+
" IMPORTANT: The user has typed a follow-up clarification while you were working." +
37+
" Call the getUserClarification tool to read it before proceeding.";
38+
3539
// Lazy-loaded ESM module reference
3640
let queryModule = null;
3741

@@ -50,6 +54,10 @@ const TEXT_STREAM_THROTTLE_MS = 50;
5054
// Pending question resolver — used by AskUserQuestion hook
5155
let _questionResolve = null;
5256

57+
// Queued clarification from the user (typed while AI is streaming)
58+
// Shape: { text: string, images: [{mediaType, base64Data}] } or null
59+
let _queuedClarification = null;
60+
5361
const nodeConnector = global.createNodeConnector(CONNECTOR_ID, exports);
5462

5563
/**
@@ -191,6 +199,7 @@ exports.cancelQuery = async function () {
191199
currentSessionId = null;
192200
// Clear any pending question
193201
_questionResolve = null;
202+
_queuedClarification = null;
194203
return { success: true };
195204
}
196205
return { success: false };
@@ -214,6 +223,46 @@ exports.answerQuestion = async function (params) {
214223
exports.destroySession = async function () {
215224
currentSessionId = null;
216225
currentAbortController = null;
226+
_queuedClarification = null;
227+
return { success: true };
228+
};
229+
230+
/**
231+
* Queue a clarification message from the user (typed while AI is streaming).
232+
* If text is already queued, appends with a newline.
233+
*/
234+
exports.queueClarification = async function (params) {
235+
const newImages = params.images || [];
236+
if (_queuedClarification) {
237+
if (params.text) {
238+
_queuedClarification.text += "\n" + params.text;
239+
}
240+
_queuedClarification.images = _queuedClarification.images.concat(newImages);
241+
} else {
242+
_queuedClarification = {
243+
text: params.text || "",
244+
images: newImages
245+
};
246+
}
247+
return { success: true };
248+
};
249+
250+
/**
251+
* Get and clear the queued clarification (text + images).
252+
* Called by the getUserClarification MCP tool.
253+
*/
254+
exports.getAndClearClarification = async function () {
255+
const result = _queuedClarification;
256+
_queuedClarification = null;
257+
return result || { text: null, images: [] };
258+
};
259+
260+
/**
261+
* Clear any queued clarification without reading it.
262+
* Used when the user clicks Edit on the queue bubble.
263+
*/
264+
exports.clearClarification = async function () {
265+
_queuedClarification = null;
217266
return { success: true };
218267
};
219268

@@ -228,7 +277,10 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
228277
try {
229278
queryFn = await getQueryFn();
230279
if (!editorMcpServer) {
231-
editorMcpServer = createEditorMcpServer(queryModule, nodeConnector);
280+
editorMcpServer = createEditorMcpServer(queryModule, nodeConnector, {
281+
hasClarification: function () { return !!_queuedClarification; },
282+
getAndClearClarification: exports.getAndClearClarification
283+
});
232284
}
233285
} catch (err) {
234286
nodeConnector.triggerPeer("aiError", {
@@ -258,7 +310,8 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
258310
"mcp__phoenix-editor__execJsInLivePreview",
259311
"mcp__phoenix-editor__controlEditor",
260312
"mcp__phoenix-editor__resizeLivePreview",
261-
"mcp__phoenix-editor__wait"
313+
"mcp__phoenix-editor__wait",
314+
"mcp__phoenix-editor__getUserClarification"
262315
],
263316
agents: {
264317
"researcher": {
@@ -292,6 +345,8 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
292345
"multiple Edit calls to make targeted changes rather than rewriting the entire " +
293346
"file with Write. This is critical because Write replaces the entire file content " +
294347
"which is slow and loses undo history." +
348+
"\n\nWhen a tool response mentions the user has typed a clarification, immediately " +
349+
"call getUserClarification to read it and incorporate the user's feedback into your current work." +
295350
(locale && !locale.startsWith("en")
296351
? "\n\nThe user's display language is " + locale + ". " +
297352
"Respond in this language unless they write in a different language."
@@ -334,6 +389,9 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
334389
" Reload when ready with execJsInLivePreview: `location.reload()`";
335390
}
336391
}
392+
if (_queuedClarification) {
393+
reason += CLARIFICATION_HINT;
394+
}
337395
return {
338396
hookSpecificOutput: {
339397
hookEventName: "PreToolUse",
@@ -370,6 +428,9 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
370428
formatted = filePath + " (" +
371429
lines.length + " lines total)\n\n" + formatted;
372430
console.log("[Phoenix AI] Serving dirty file content for:", filePath);
431+
if (_queuedClarification) {
432+
formatted += CLARIFICATION_HINT;
433+
}
373434
return {
374435
hookSpecificOutput: {
375436
hookEventName: "PreToolUse",
@@ -419,6 +480,9 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale,
419480
" Reload when ready with execJsInLivePreview: `location.reload()`";
420481
}
421482
}
483+
if (_queuedClarification) {
484+
reason += CLARIFICATION_HINT;
485+
}
422486
return {
423487
hookSpecificOutput: {
424488
hookEventName: "PreToolUse",

0 commit comments

Comments
 (0)