Skip to content

Commit 7edf6e8

Browse files
authored
Merge pull request #2 from marimo-team/ms/session-modes-and-commands
feat: session modes and slash commands
2 parents 25eb9b3 + 2d2bd8f commit 7edf6e8

13 files changed

Lines changed: 222 additions & 56 deletions

File tree

demo/acp-demo.tsx

Lines changed: 121 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ function AcpDemo() {
9292
resolvePermission,
9393
rejectPermission,
9494
agent: acp,
95+
availableCommands,
96+
sessionMode,
9597
} = useAcpClient({
9698
wsUrl,
9799
reconnectAttempts: 3,
@@ -163,6 +165,19 @@ function AcpDemo() {
163165
}, [acp, activeSessionId]),
164166
);
165167

168+
const [executeSetMode, isSettingMode, setModeError] = useAsync(
169+
useCallback(
170+
async (modeId: string) => {
171+
if (!acp || !activeSessionId) throw new Error("ACP not connected or no active session");
172+
if (!("setSessionMode" in acp) || typeof acp.setSessionMode !== "function") {
173+
throw new Error("Agent does not support session modes");
174+
}
175+
return acp.setSessionMode({ sessionId: activeSessionId, modeId });
176+
},
177+
[acp, activeSessionId],
178+
),
179+
);
180+
166181
const handleConnect = async () => {
167182
try {
168183
await connect();
@@ -227,6 +242,17 @@ function AcpDemo() {
227242
setActiveSessionId(null);
228243
};
229244

245+
const handleSlashCommand = (commandName: string) => {
246+
setPromptText((prev) => `${prev}/${commandName} `);
247+
};
248+
249+
const handleSetSessionMode = async (modeId: string) => {
250+
const response = await executeSetMode(modeId);
251+
if (response) {
252+
console.log("Session mode changed:", response);
253+
}
254+
};
255+
230256
const getStatusColor = () => {
231257
switch (connectionState.status) {
232258
case "connected":
@@ -339,11 +365,14 @@ function AcpDemo() {
339365
)}
340366
</div>
341367

342-
{(newSessionError || promptError || cancelError) && (
368+
{(newSessionError || promptError || cancelError || setModeError) && (
343369
<div className="bg-red-50 border border-red-200 rounded-md p-3 mb-3">
344370
<h4 className="font-medium text-red-800 text-sm mb-1">Error</h4>
345371
<p className="text-sm text-red-700">
346-
{newSessionError?.message || promptError?.message || cancelError?.message}
372+
{newSessionError?.message ||
373+
promptError?.message ||
374+
cancelError?.message ||
375+
setModeError?.message}
347376
</p>
348377
</div>
349378
)}
@@ -405,42 +434,101 @@ function AcpDemo() {
405434
)}
406435

407436
{activeSessionId && (
408-
<div className="space-y-2 pt-2 border-t">
409-
<label
410-
htmlFor="promptText"
411-
className="block text-sm font-medium text-gray-700"
412-
>
413-
Send Message to Agent
414-
</label>
415-
<div className="flex gap-2">
416-
<input
417-
id="promptText"
418-
type="text"
419-
value={promptText}
420-
onChange={(e) => setPromptText(e.target.value)}
421-
onKeyPress={(e) => e.key === "Enter" && !isPrompting && handlePrompt()}
422-
placeholder="Type your message..."
423-
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
424-
disabled={isPrompting}
425-
/>
426-
<button
427-
type="button"
428-
onClick={handlePrompt}
429-
disabled={isPrompting || !promptText.trim()}
430-
className="px-3 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 disabled:bg-gray-400 disabled:cursor-not-allowed text-sm"
437+
<div className="space-y-3 pt-2 border-t">
438+
{/* Slash Commands */}
439+
{availableCommands.length > 0 && (
440+
<div>
441+
<label
442+
htmlFor="slashCommands"
443+
className="block text-sm font-medium text-gray-700 mb-2"
444+
>
445+
Slash Commands
446+
</label>
447+
<select
448+
id="slashCommands"
449+
onChange={(e) => {
450+
if (e.target.value) {
451+
handleSlashCommand(e.target.value);
452+
e.target.value = "";
453+
}
454+
}}
455+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
456+
>
457+
<option value="">Insert slash command...</option>
458+
{availableCommands.map((command) => (
459+
<option key={command.name} value={command.name}>
460+
/{command.name} - {command.description}
461+
</option>
462+
))}
463+
</select>
464+
</div>
465+
)}
466+
467+
{/* Session Modes */}
468+
{sessionMode && sessionMode.availableModes.length > 1 && (
469+
<div>
470+
<label
471+
htmlFor="sessionModes"
472+
className="block text-sm font-medium text-gray-700 mb-2"
473+
>
474+
Session Mode
475+
</label>
476+
<select
477+
id="sessionModes"
478+
value={sessionMode.currentModeId}
479+
onChange={(e) => handleSetSessionMode(e.target.value)}
480+
disabled={isSettingMode}
481+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm disabled:bg-gray-100"
482+
>
483+
{sessionMode.availableModes.map((mode) => (
484+
<option key={mode.id} value={mode.id}>
485+
{mode.name} {mode.description ? `- ${mode.description}` : ""}
486+
</option>
487+
))}
488+
</select>
489+
{isSettingMode && (
490+
<div className="text-xs text-blue-600 mt-1">Changing mode...</div>
491+
)}
492+
</div>
493+
)}
494+
495+
<div>
496+
<label
497+
htmlFor="promptText"
498+
className="block text-sm font-medium text-gray-700 mb-2"
431499
>
432-
{isPrompting ? "..." : "Send"}
433-
</button>
434-
{isPrompting && (
500+
Send Message to Agent
501+
</label>
502+
<div className="flex gap-2">
503+
<input
504+
id="promptText"
505+
type="text"
506+
value={promptText}
507+
onChange={(e) => setPromptText(e.target.value)}
508+
onKeyPress={(e) => e.key === "Enter" && !isPrompting && handlePrompt()}
509+
placeholder="Type your message..."
510+
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
511+
disabled={isPrompting}
512+
/>
435513
<button
436514
type="button"
437-
onClick={handleCancel}
438-
disabled={isCancelling}
439-
className="px-3 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 disabled:bg-gray-400 disabled:cursor-not-allowed text-sm"
515+
onClick={handlePrompt}
516+
disabled={isPrompting || !promptText.trim()}
517+
className="px-3 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 disabled:bg-gray-400 disabled:cursor-not-allowed text-sm"
440518
>
441-
{isCancelling ? "..." : "Cancel"}
519+
{isPrompting ? "..." : "Send"}
442520
</button>
443-
)}
521+
{isPrompting && (
522+
<button
523+
type="button"
524+
onClick={handleCancel}
525+
disabled={isCancelling}
526+
className="px-3 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 disabled:bg-gray-400 disabled:cursor-not-allowed text-sm"
527+
>
528+
{isCancelling ? "..." : "Cancel"}
529+
</button>
530+
)}
531+
</div>
444532
</div>
445533
</div>
446534
)}

demo/components/content-block.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ContentBlock } from "@zed-industries/agent-client-protocol";
1+
import type { ContentBlock } from "@zed-industries/agent-client-protocol/typescript/acp.js";
22
import type React from "react";
33
import { Streamdown } from "streamdown";
44
import { logNever } from "../../src/utils/never.js";

demo/components/timeline-components.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
/** biome-ignore-all lint/suspicious/noArrayIndexKey: just a demo */
22

3-
import type { ToolCallContent, ToolCallUpdate } from "@zed-industries/agent-client-protocol";
3+
import type {
4+
ToolCallContent,
5+
ToolCallUpdate,
6+
} from "@zed-industries/agent-client-protocol/typescript/acp.js";
47
import type React from "react";
58
import { useEffect, useRef } from "react";
69
import type {
@@ -230,6 +233,8 @@ function ToolCallContentBlockComponent({ content }: { content: ToolCallContent }
230233
return <ContentBlockComponent content={content.content} />;
231234
case "diff":
232235
return <DiffContent diff={content} />;
236+
case "terminal":
237+
return <div className="text-xs text-gray-500">{content.terminalId}</div>;
233238
default:
234239
logNever(content);
235240
return (
@@ -262,6 +267,14 @@ function SessionNotificationRenderer({ sessionData }: { sessionData: SessionUpda
262267
case "plan":
263268
return <PlanNotificationComponent sessionUpdate={sessionData} />;
264269

270+
case "available_commands_update":
271+
return null;
272+
273+
case "current_mode_update":
274+
return (
275+
<div className="text-xs text-gray-500">Switched to mode: {sessionData.currentModeId}</div>
276+
);
277+
265278
default:
266279
logNever(sessionData);
267280
return (

demo/components/tool-call.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ToolCallUpdate } from "@zed-industries/agent-client-protocol";
1+
import type { ToolCallUpdate } from "@zed-industries/agent-client-protocol/typescript/acp.js";
22
import type { ToolCallDiff, ToolCallStart } from "../../src/client/types.js";
33
import { ContentBlockComponent } from "./content-block.js";
44
import { Badge, Card, CodeBlock, IconLabel, SectionHeader } from "./ui-primitives.js";

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
]
7575
},
7676
"dependencies": {
77-
"@zed-industries/agent-client-protocol": "^0.1.2",
77+
"@zed-industries/agent-client-protocol": "^0.3.0",
7878
"zustand": "^5.0.2"
7979
}
8080
}

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/client/acp-client.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
InitializeRequest,
77
InitializeResponse,
88
LoadSessionRequest,
9+
LoadSessionResponse,
910
NewSessionRequest,
1011
NewSessionResponse,
1112
PromptRequest,
@@ -17,7 +18,7 @@ import type {
1718
SessionNotification,
1819
WriteTextFileRequest,
1920
WriteTextFileResponse,
20-
} from "@zed-industries/agent-client-protocol";
21+
} from "@zed-industries/agent-client-protocol/typescript/acp.js";
2122
import { Deferred } from "../utils/deferred.js";
2223

2324
export interface AcpClientOptions {
@@ -130,11 +131,12 @@ export class ListeningAgent implements Agent {
130131
});
131132
}
132133

133-
async loadSession(params: LoadSessionRequest): Promise<void> {
134+
async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
134135
this.callbacks.on_loadSession_start?.(params);
135136
if (this.agent.loadSession) {
136-
return this.agent.loadSession(params).then(() => {
137-
this.callbacks.on_loadSession_response?.(undefined, params);
137+
return this.agent.loadSession(params).then((response) => {
138+
this.callbacks.on_loadSession_response?.(response, params);
139+
return response;
138140
});
139141
}
140142
throw new Error("Agent does not support loadSession capability");

src/client/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import type { SessionNotification, ToolCallContent } from "@zed-industries/agent-client-protocol";
1+
import type {
2+
SessionNotification,
3+
ToolCallContent,
4+
} from "@zed-industries/agent-client-protocol/typescript/acp.js";
25

36
export type SessionUpdate = SessionNotification["update"];
47

0 commit comments

Comments
 (0)