Skip to content

Commit 395f567

Browse files
committed
Send desktop detail text directly
1 parent e2c7071 commit 395f567

9 files changed

Lines changed: 51 additions & 30 deletions

File tree

docs/architecture/communication-brain.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ It does not own:
1919

2020
Core communication policy:
2121

22-
- typed voice/text runtime input follows the v1 path: utterance -> structured Draft -> Dispatch Plan -> Dispatch Gate -> Task
22+
- typed voice/ASR runtime input follows the v1 path: utterance -> structured Draft -> Dispatch Plan -> Dispatch Gate -> Task
23+
- desktop Bro Detail typed input in push-to-talk mode is a direct targeted Communication Brain message to the selected Bro, not a Draft preparation/confirmation surface
2324
- the dispatch gate is deterministic and is the final authority for starting execution
2425
- the default execution mode for draft-created work is read-only/proposal-first; code modification and side effects require explicit confirmation
2526
- free-form utterance meaning is produced by the Communication Brain interaction-classifier boundary, not by runtime transcript keyword checks

docs/guides/frontend-workbench.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ Current behavior:
9090
- Bro Detail draft input uses a separate connector-managed Agora STT path: the
9191
page first prepares a fresh Agora-safe channel and browser RTC token, then
9292
starts the ASR bot after the browser joins RTC with the microphone disabled
93+
- Desktop Bro Detail typed input in push-to-talk mode bypasses the Draft card:
94+
submitting the composer sends a targeted session message directly to the
95+
selected Bro instead of calling draft ASR or draft Send endpoints
9396
- Bro Detail does not use the shell `session_id` as the Agora channel name;
9497
each page start receives a unique channel from the connector to avoid channel
9598
conflicts

docs/memories.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,4 @@ Short log of important design decisions and changes for Newbro.
237237
- Limited the active Newbro UI to artboarded Home, Bro Detail, mobile, onboarding, create/connect, and offline states; standalone Bros, Nodes, Settings, and custom fallback product screens are removed or rendered blank.
238238
- Changed first-run Bro setup so Home issues a user-owned executor-node command first and creates/binds the Bro persona only after that node records its first successful connection.
239239
- Made connected-node persona creation idempotent per user and executor node so repeated first-run completion requests return the existing Bro instead of creating duplicates.
240+
- Changed desktop Bro Detail typed input in push-to-talk mode to send a targeted Communication Brain message directly to the selected Bro instead of preparing a Draft and requiring a separate Send confirmation.

docs/protocol/draft-to-execute.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Draft-to-Execute Protocol
22

3-
`newbro v1` uses a quiet, voice/text-driven draft-to-execute workflow.
3+
`newbro v1` uses a quiet, voice/ASR-driven draft-to-execute workflow.
4+
Desktop Bro Detail typed input in push-to-talk mode is the exception: it sends a
5+
targeted Communication Brain message directly to the selected Bro instead of
6+
preparing a Draft that needs a separate Send confirmation.
47

58
The stable contract is:
69

src/newbro/ui/src/ArtboardShell.tsx

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -899,32 +899,14 @@ function DesktopComposerBar({
899899
}) {
900900
const shell = useNewbroShell();
901901
const [draft, setDraft] = useState("");
902-
const draftText = shell.draftSession?.current_draft?.text ?? draft;
903902
const connected = shell.voiceSession.phase === "connected";
904903
const loading = shell.voiceSession.phase === "loading";
905904

906905
function submitText(event: FormEvent<HTMLFormElement>) {
907906
event.preventDefault();
908907
const text = draft.trim();
909908
if (!text || disabled) return;
910-
shell.sendMessage(text);
911-
shell.submitDraftAsrTurn({ raw_text: text, assigned_bro_id: bro.id });
912-
setDraft("");
913-
}
914-
915-
async function sendCurrentDraft() {
916-
if (!shell.activeShellSessionId || disabled) return;
917-
await sendDraft(shell.activeShellSessionId, {
918-
draft_session_id: shell.draftSession?.id,
919-
draft_revision_id: shell.draftSession?.current_revision_id ?? undefined,
920-
});
921-
await shell.refreshShellSession();
922-
}
923-
924-
async function clearCurrentDraft() {
925-
if (!shell.activeShellSessionId) return;
926-
await clearDraft(shell.activeShellSessionId, { draft_session_id: shell.draftSession?.id });
927-
await shell.refreshShellSession();
909+
shell.sendMessage(text, bro.id);
928910
setDraft("");
929911
}
930912

@@ -943,7 +925,7 @@ function DesktopComposerBar({
943925
</div>
944926
<span className="dt-cmp-hint">
945927
<kbd className="dt-kbd">space</kbd>
946-
{disabled ? "node required before sending" : "push to talk anywhere"}
928+
{disabled ? "node required before sending" : "type sends directly"}
947929
</span>
948930
</div>
949931
<div className="dt-cmp-bar">
@@ -956,7 +938,7 @@ function DesktopComposerBar({
956938
placeholder={disabled ? "Reconnect the node before sending" : `Type to ${bro.name}...`}
957939
disabled={disabled}
958940
/>
959-
<button type="button" className="dt-cmp-mode nb-detail-clear" onClick={() => { void clearCurrentDraft(); }} disabled={!draftText}>
941+
<button type="button" className="dt-cmp-mode nb-detail-clear" onClick={() => setDraft("")} disabled={!draft}>
960942
Clear
961943
</button>
962944
<button
@@ -970,15 +952,13 @@ function DesktopComposerBar({
970952
<Mic size={18} aria-hidden="true" />
971953
</button>
972954
<button
973-
type="button"
955+
type="submit"
974956
className="dt-cmp-send"
975957
aria-label="Send message"
976-
disabled={disabled || !draftText}
977-
onClick={() => { void sendCurrentDraft(); }}
958+
disabled={disabled || !draft.trim()}
978959
>
979960
<SendHorizontal size={16} strokeWidth={2.2} />
980961
</button>
981-
<button type="submit" className="sr-only">Send message</button>
982962
</div>
983963
</form>
984964
);

src/newbro/ui/src/NewbroShell.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -500,11 +500,11 @@ function useNewbroShellState() {
500500
setShellWarning(null);
501501
});
502502

503-
const sendMessage = useCallback((text: string): boolean => {
503+
const sendMessage = useCallback((text: string, targetPersonaId?: string | null): boolean => {
504504
const socket = socketRef.current;
505505
if (!socket || socket.readyState !== WebSocket.OPEN) return false;
506506
const requestId = `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
507-
sendSocketMessage(socket, requestId, text);
507+
sendSocketMessage(socket, requestId, text, targetPersonaId);
508508
return true;
509509
}, []);
510510

src/newbro/ui/src/__tests__/App.test.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,24 @@ describe("Newbro artboard shell", () => {
347347
await waitFor(() => expect(clientMock.setVoiceTarget).toHaveBeenCalledWith("session-existing", "forge"));
348348
});
349349

350+
it("sends desktop typed Bro detail input directly to the targeted Bro", async () => {
351+
window.history.replaceState({}, "", "/bros/forge?sid=session-existing");
352+
353+
render(<RouterProvider router={getRouter()} />);
354+
355+
expect(await screen.findByRole("heading", { name: "Forge" })).toBeInTheDocument();
356+
fireEvent.change(screen.getByLabelText("Message"), {
357+
target: { value: "Run the desktop direct send path" },
358+
});
359+
fireEvent.click(screen.getByRole("button", { name: "Send message" }));
360+
361+
await waitFor(() => expect(clientMock.sendSocketMessage).toHaveBeenCalled());
362+
expect(clientMock.sendSocketMessage.mock.calls.at(-1)?.[2]).toBe("Run the desktop direct send path");
363+
expect(clientMock.sendSocketMessage.mock.calls.at(-1)?.[3]).toBe("forge");
364+
expect(clientMock.sendSocketDraftAsrTurn).not.toHaveBeenCalled();
365+
expect(clientMock.sendDraft).not.toHaveBeenCalled();
366+
});
367+
350368
it("uses the artboarded offline detail state and blocks talk/send for disconnected usable nodes", async () => {
351369
const offlineNode = usableExecutorNode({
352370
connected_executors: [],

src/newbro/ui/src/lib/session-client.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,20 @@ describe("session-client transport base URL handling", () => {
147147
expect(revealed.token).toBe("token-1");
148148
});
149149

150+
it("includes the target persona on websocket messages when provided", async () => {
151+
const client = await import("./session-client");
152+
const socket = { send: vi.fn() } as unknown as WebSocket;
153+
154+
client.sendSocketMessage(socket, "req-1", "Run this", "forge");
155+
156+
expect(socket.send).toHaveBeenCalledWith(JSON.stringify({
157+
type: "send_message",
158+
request_id: "req-1",
159+
text: "Run this",
160+
target_persona_id: "forge",
161+
}));
162+
});
163+
150164
it("submits task commands to the session commands endpoint", async () => {
151165
const fetchMock = vi.fn(async () =>
152166
okJsonResponse({

src/newbro/ui/src/lib/session-client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,12 +284,13 @@ export function openSessionStream(
284284
return openSocket<SessionStreamEvent>(`${API_PREFIX}/sessions/${sessionId}/stream`, handlers);
285285
}
286286

287-
export function sendSocketMessage(socket: WebSocket, requestId: string, text: string) {
287+
export function sendSocketMessage(socket: WebSocket, requestId: string, text: string, targetPersonaId?: string | null) {
288288
socket.send(
289289
JSON.stringify({
290290
type: "send_message",
291291
request_id: requestId,
292292
text,
293+
...(targetPersonaId ? { target_persona_id: targetPersonaId } : {}),
293294
}),
294295
);
295296
}

0 commit comments

Comments
 (0)