Skip to content

Commit c64d9a1

Browse files
committed
style(toolbar): refactor UI and karton instantiation
1 parent 9630162 commit c64d9a1

30 files changed

+688
-963
lines changed

agent/client/src/Agent.ts

Lines changed: 179 additions & 175 deletions
Large diffs are not rendered by default.

agent/client/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
import { Agent } from './Agent.js';
22

33
export { Agent };
4+
export type {
5+
AgentCallbacks,
6+
AgentProcedures,
7+
} from './types/agent-callbacks.js';
8+
export type { AgentEvent, AgentEventCallback } from './utils/event-utils.js';
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { KartonContract, ChatMessage } from '@stagewise/karton-contract';
2+
3+
type KartonState = KartonContract['state'];
4+
type StateRecipe = (draft: KartonState) => void;
5+
6+
export type AgentStateGetter = () => KartonState;
7+
export type AgentStateSetter = (recipe: StateRecipe) => KartonState;
8+
9+
export interface AgentCallbacks {
10+
getState: AgentStateGetter;
11+
setState: AgentStateSetter;
12+
}
13+
14+
export interface AgentProcedures {
15+
undoToolCallsUntilUserMessage: (
16+
userMessageId: string,
17+
chatId: string,
18+
) => Promise<void>;
19+
undoToolCallsUntilLatestUserMessage: (
20+
chatId: string,
21+
) => Promise<ChatMessage | null>;
22+
retrySendingUserMessage: () => Promise<void>;
23+
refreshSubscription: () => Promise<void>;
24+
abortAgentCall: () => Promise<void>;
25+
approveToolCall: (
26+
toolCallId: string,
27+
callingClientId: string,
28+
) => Promise<void>;
29+
rejectToolCall: (
30+
toolCallId: string,
31+
callingClientId: string,
32+
) => Promise<void>;
33+
createChat: () => Promise<string>;
34+
switchChat: (chatId: string, callingClientId: string) => Promise<void>;
35+
deleteChat: (chatId: string, callingClientId: string) => Promise<void>;
36+
sendUserMessage: (
37+
message: ChatMessage,
38+
callingClientId: string,
39+
) => Promise<void>;
40+
assistantMadeCodeChangesUntilLatestUserMessage: (
41+
chatId: string,
42+
) => Promise<boolean>;
43+
}

agent/client/src/utils/karton-helpers.ts

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,27 @@
1-
import type { KartonContract, ChatMessage } from '@stagewise/karton-contract';
2-
import type { KartonServer } from '@stagewise/karton/server';
1+
import type { ChatMessage } from '@stagewise/karton-contract';
32
import type { ToolCallProcessingResult } from './tool-call-utils.js';
43
import type { InferUIMessageChunk, ToolUIPart } from 'ai';
4+
import type { AgentCallbacks } from '../types/agent-callbacks.js';
55

66
/**
77
* Checks if a message with the given ID exists in the active chat
8-
* @param karton - The Karton server instance containing chat state
8+
* @param callbacks - The agent callbacks for state access
99
* @param messageId - The unique identifier of the message to check
1010
* @returns True if the message exists in the active chat, false otherwise
1111
*/
12-
function messageExists(
13-
karton: KartonServer<KartonContract>,
14-
messageId: string,
15-
): boolean {
16-
return karton.state.chats[karton.state.activeChatId!]!.messages.some(
17-
(m) => m.id === messageId,
12+
function messageExists(callbacks: AgentCallbacks, messageId: string): boolean {
13+
const state = callbacks.getState();
14+
return state.chats[state.activeChatId!]!.messages.some(
15+
(m: ChatMessage) => m.id === messageId,
1816
);
1917
}
2018

2119
/**
2220
* Creates a new chat with a timestamped title and sets it as the active chat
23-
* @param karton - The Karton server instance to modify
21+
* @param callbacks - The agent callbacks for state modification
2422
* @returns The unique ID of the newly created chat
2523
*/
26-
export function createAndActivateNewChat(karton: KartonServer<KartonContract>) {
24+
export function createAndActivateNewChat(callbacks: AgentCallbacks) {
2725
const chatId = crypto.randomUUID();
2826
const title = `New Chat - ${new Date().toLocaleString('en-US', {
2927
month: 'short',
@@ -32,7 +30,7 @@ export function createAndActivateNewChat(karton: KartonServer<KartonContract>) {
3230
minute: '2-digit',
3331
hour12: true,
3432
})}`;
35-
karton.setState((draft) => {
33+
callbacks.setState((draft) => {
3634
draft.chats[chatId] = {
3735
title,
3836
createdAt: new Date(),
@@ -46,21 +44,22 @@ export function createAndActivateNewChat(karton: KartonServer<KartonContract>) {
4644
/**
4745
* Appends text content to a message, creating the message if it doesn't exist
4846
* or creating/appending to a text part at the specified index
49-
* @param karton - The Karton server instance to modify
47+
* @param callbacks - The agent callbacks for state modification
5048
* @param messageId - The unique identifier of the message to append to
5149
* @param delta - The text content to append
5250
* @param partIndex - The index of the message part to append to
5351
*/
5452
export function appendTextDeltaToMessage(
55-
karton: KartonServer<KartonContract>,
53+
callbacks: AgentCallbacks,
5654
messageId: string,
5755
delta: string,
5856
partIndex: number,
5957
) {
6058
// If the message doesn't exist, create it
61-
if (!messageExists(karton, messageId)) {
62-
karton.setState((draft) => {
63-
draft.chats[karton.state.activeChatId!]!.messages.push({
59+
if (!messageExists(callbacks, messageId)) {
60+
callbacks.setState((draft) => {
61+
const state = callbacks.getState();
62+
draft.chats[state.activeChatId!]!.messages.push({
6463
role: 'assistant',
6564
id: messageId,
6665
parts: [{ type: 'text', text: delta }],
@@ -71,9 +70,10 @@ export function appendTextDeltaToMessage(
7170
});
7271
} else {
7372
// If the message exists, create a text part or append to the existing one
74-
karton.setState((draft) => {
75-
const message = draft.chats[karton.state.activeChatId!]!.messages.find(
76-
(m) => m.id === messageId,
73+
callbacks.setState((draft) => {
74+
const state = callbacks.getState();
75+
const message = draft.chats[state.activeChatId!]!.messages.find(
76+
(m: ChatMessage) => m.id === messageId,
7777
)!;
7878

7979
// Create a new part if it's a new one
@@ -96,28 +96,29 @@ export function appendTextDeltaToMessage(
9696
/**
9797
* Appends tool input information to a message, creating the message if it doesn't exist
9898
* or updating the tool part at the specified index
99-
* @param karton - The Karton server instance to modify
99+
* @param callbacks - The agent callbacks for state modification
100100
* @param messageId - The unique identifier of the message to append to
101101
* @param chunk - The tool input chunk containing tool call details
102102
* @param partIndex - The index of the message part to update
103103
*/
104104
export function appendToolInputToMessage(
105-
karton: KartonServer<KartonContract>,
105+
callbacks: AgentCallbacks,
106106
messageId: string,
107107
chunk: Extract<
108108
InferUIMessageChunk<ChatMessage>,
109109
{ type: 'tool-input-available' }
110110
>,
111111
partIndex: number,
112112
) {
113-
karton.setState((draft) => {
114-
const message = draft.chats[karton.state.activeChatId!]!.messages.find(
115-
(m) => m.id === messageId,
113+
callbacks.setState((draft) => {
114+
const state = callbacks.getState();
115+
const message = draft.chats[state.activeChatId!]!.messages.find(
116+
(m: ChatMessage) => m.id === messageId,
116117
);
117118

118119
if (!message) {
119120
// If the message doesn't exist, create it
120-
draft.chats[karton.state.activeChatId!]!.messages.push({
121+
draft.chats[state.activeChatId!]!.messages.push({
121122
role: 'assistant',
122123
id: messageId,
123124
parts: [
@@ -159,23 +160,24 @@ export function appendToolInputToMessage(
159160
/**
160161
* Attaches tool execution results to the corresponding tool parts in a message
161162
* Updates the tool part state to reflect success or error outcomes
162-
* @param karton - The Karton server instance to modify
163+
* @param callbacks - The agent callbacks for state modification
163164
* @param toolResults - Array of tool execution results to attach
164165
* @param messageId - The unique identifier of the message containing the tool parts
165166
*/
166167
export function attachToolOutputToMessage(
167-
karton: KartonServer<KartonContract>,
168+
callbacks: AgentCallbacks,
168169
toolResults: ToolCallProcessingResult[],
169170
messageId: string,
170171
) {
171-
karton.setState((draft) => {
172-
const message = draft.chats[karton.state.activeChatId!]!.messages.find(
173-
(m) => m.id === messageId,
172+
callbacks.setState((draft) => {
173+
const state = callbacks.getState();
174+
const message = draft.chats[state.activeChatId!]!.messages.find(
175+
(m: ChatMessage) => m.id === messageId,
174176
);
175177
if (!message) return;
176178
for (const result of toolResults) {
177179
const part = message.parts.find(
178-
(p) => 'toolCallId' in p && p.toolCallId === result.toolCallId,
180+
(p: any) => 'toolCallId' in p && p.toolCallId === result.toolCallId,
179181
);
180182
if (!part) continue;
181183
if (part.type !== 'dynamic-tool' && !part.type.startsWith('tool-'))
@@ -196,14 +198,16 @@ export function attachToolOutputToMessage(
196198

197199
/**
198200
* Finds tool calls in the last assistant message that don't have corresponding results
201+
* @param callbacks - The agent callbacks for state access
199202
* @param chatId - The chat ID to check
200203
* @returns Array of pending tool call IDs and their names
201204
*/
202205
export function findPendingToolCalls(
203-
karton: KartonServer<KartonContract>,
206+
callbacks: AgentCallbacks,
204207
chatId: string,
205208
): Array<{ toolCallId: string }> {
206-
const chat = karton.state.chats[chatId];
209+
const state = callbacks.getState();
210+
const chat = state.chats[chatId];
207211
if (!chat) return [];
208212

209213
const messages = chat.messages;

apps/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
},
3434
"devDependencies": {
3535
"@stagewise/karton": "workspace:*",
36+
"@stagewise/karton-contract": "workspace:*",
3637
"@inquirer/prompts": "^7.0.0",
3738
"@stagewise-plugins/angular": "workspace:*",
3839
"@stagewise-plugins/react": "workspace:*",

apps/cli/src/server/agent-loader.ts

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import { printInfoMessages } from '@/utils/print-info-messages.js';
22
import { log } from '../utils/logger.js';
33
import configResolver from '@/config/index.js';
4-
import { Agent } from '@stagewise/agent-client';
4+
import { Agent, type AgentCallbacks } from '@stagewise/agent-client';
55
import { ClientRuntimeNode } from '@stagewise/agent-runtime-node';
66
import { analyticsEvents } from '@/utils/telemetry.js';
7+
import {
8+
createKartonServer,
9+
type KartonServer,
10+
} from '@stagewise/karton/server';
11+
import type { KartonContract } from '@stagewise/karton-contract';
712

813
let agentInstance: Agent | null = null;
14+
let kartonServer: KartonServer<KartonContract> | null = null;
915

1016
/**
1117
* Loads and initializes the agent server
@@ -31,7 +37,23 @@ export async function loadAndInitializeAgent(
3137
workingDirectory: config.dir,
3238
});
3339

34-
// Create agent instance
40+
// Create a placeholder for the karton server
41+
let tempKartonServer: KartonServer<KartonContract> | null = null;
42+
43+
// Create callbacks that will use the karton server (will be set later)
44+
const callbacks: AgentCallbacks = {
45+
getState: () => {
46+
if (!tempKartonServer) throw new Error('Karton server not initialized');
47+
return tempKartonServer.state;
48+
},
49+
setState: (recipe) => {
50+
if (!tempKartonServer) throw new Error('Karton server not initialized');
51+
// @ts-ignore we'll fix this whole temp instantiation shit later
52+
return tempKartonServer.setState(recipe);
53+
},
54+
};
55+
56+
// Create agent instance with callbacks
3557
agentInstance = Agent.getInstance({
3658
clientRuntime,
3759
accessToken,
@@ -55,17 +77,36 @@ export async function loadAndInitializeAgent(
5577
break;
5678
}
5779
},
80+
callbacks,
5881
});
5982

60-
// Initialize agent with Express integration
61-
// This will automatically set up the Karton endpoint
62-
const agentServer = await agentInstance.initialize();
83+
// Now create the karton server with agent procedures
84+
kartonServer = await createKartonServer<KartonContract>({
85+
procedures: agentInstance.getAgentProcedures() as any,
86+
initialState: {
87+
workspaceInfo: {
88+
path: clientRuntime.fileSystem.getCurrentWorkingDirectory(),
89+
devAppPort: 0,
90+
loadedPlugins: [],
91+
},
92+
activeChatId: null,
93+
chats: {},
94+
isWorking: false,
95+
toolCallApprovalRequests: [],
96+
subscription: undefined,
97+
},
98+
});
99+
100+
// Set the karton server reference in callbacks
101+
tempKartonServer = kartonServer;
102+
103+
// Initialize the agent
104+
await agentInstance.initialize();
63105

64-
// Return the WebSocket server instance if available
65-
// The agent SDK may not return the WebSocket server in current versions
106+
// Return the WebSocket server instance from karton server
66107
return {
67108
success: true,
68-
wss: agentServer.wss,
109+
wss: kartonServer.wss,
69110
};
70111
} catch (error) {
71112
log.error(
@@ -82,15 +123,31 @@ export function shutdownAgent(): void {
82123
if (agentInstance?.shutdown) {
83124
try {
84125
agentInstance.shutdown();
85-
log.debug('Agent server shut down successfully');
126+
log.debug('Agent shut down successfully');
86127
} catch (error) {
87128
log.error(
88129
`Error shutting down agent: ${error instanceof Error ? error.message : 'Unknown error'}`,
89130
);
90131
}
91132
}
92-
// Clear the instance reference
133+
134+
if (kartonServer) {
135+
try {
136+
// Close WebSocket server
137+
if (kartonServer.wss) {
138+
kartonServer.wss.close();
139+
}
140+
log.debug('Karton server shut down successfully');
141+
} catch (error) {
142+
log.error(
143+
`Error shutting down karton server: ${error instanceof Error ? error.message : 'Unknown error'}`,
144+
);
145+
}
146+
}
147+
148+
// Clear the instance references
93149
agentInstance = null;
150+
kartonServer = null;
94151
}
95152

96153
export function getAgentInstance(): any {

packages/karton-contract/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export type AgentError = {
4343

4444
type AppState = {
4545
activeChatId: ChatId | null;
46+
workspacePath: string | null;
4647
chats: Record<ChatId, Chat>;
4748
toolCallApprovalRequests: string[];
4849
isWorking: boolean;

packages/stage-ui/src/components/button.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ export const buttonVariants = cva(
1313
ghost: 'bg-transparent font-medium text-foreground hover:bg-zinc-500/5',
1414
},
1515
size: {
16+
xs: 'h-6 rounded-xl px-2 py-1 text-xs',
1617
sm: 'h-8 rounded-xl px-2 py-1 text-sm',
1718
md: 'h-10 rounded-xl px-4 py-2 text-sm',
1819
lg: 'h-12 rounded-xl px-6 py-3 text-base',
1920
xl: 'h-14 rounded-xl px-8 py-4 text-lg',
21+
'icon-xs': 'size-6 rounded-full',
2022
'icon-sm': 'size-8 rounded-full',
2123
'icon-md': 'size-10 rounded-full',
2224
},

packages/stage-ui/src/components/menu.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export function MenuItem({ children, className, ...props }: MenuItemProps) {
6767
<MenuBase.Item
6868
{...props}
6969
className={cn(
70-
'flex w-full min-w-24 cursor-default flex-row items-center justify-start gap-2 rounded-md py-1.5 pr-6 pl-2 text-sm transition-all duration-150 ease-out hover:bg-black/5 hover:pl-2.25 dark:hover:bg-white/5',
70+
'flex w-full min-w-24 cursor-default flex-row items-center justify-start gap-2 rounded-md py-1.5 pr-6 pl-2 text-sm transition-all duration-150 ease-out hover:bg-black/5 hover:pr-5.75 hover:pl-2.25 dark:hover:bg-white/5',
7171
className,
7272
)}
7373
>
@@ -96,7 +96,7 @@ export function MenuSubmenuTrigger({
9696
<MenuBase.SubmenuTrigger
9797
{...props}
9898
className={cn(
99-
'group flex w-full min-w-24 cursor-default flex-row items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm transition-all duration-150 ease-out hover:bg-black/5 hover:pl-2.25 dark:hover:bg-white/5',
99+
'group flex w-full min-w-24 cursor-default flex-row items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm transition-all duration-150 ease-out hover:bg-black/5 hover:pr-1.75 hover:pl-2.25 dark:hover:bg-white/5',
100100
className,
101101
)}
102102
>

0 commit comments

Comments
 (0)