@townco/ui 0.1.15 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/core/hooks/index.d.ts +1 -0
  2. package/dist/core/hooks/index.js +1 -0
  3. package/dist/core/hooks/use-chat-messages.d.ts +50 -11
  4. package/dist/core/hooks/use-chat-session.d.ts +5 -5
  5. package/dist/core/hooks/use-tool-calls.d.ts +52 -0
  6. package/dist/core/hooks/use-tool-calls.js +61 -0
  7. package/dist/core/schemas/chat.d.ts +166 -83
  8. package/dist/core/schemas/chat.js +27 -27
  9. package/dist/core/schemas/index.d.ts +1 -0
  10. package/dist/core/schemas/index.js +1 -0
  11. package/dist/core/schemas/tool-call.d.ts +174 -0
  12. package/dist/core/schemas/tool-call.js +130 -0
  13. package/dist/core/store/chat-store.d.ts +28 -28
  14. package/dist/core/store/chat-store.js +123 -59
  15. package/dist/gui/components/ChatLayout.js +11 -10
  16. package/dist/gui/components/MessageContent.js +4 -1
  17. package/dist/gui/components/ToolCall.d.ts +8 -0
  18. package/dist/gui/components/ToolCall.js +100 -0
  19. package/dist/gui/components/ToolCallList.d.ts +9 -0
  20. package/dist/gui/components/ToolCallList.js +22 -0
  21. package/dist/gui/components/index.d.ts +2 -0
  22. package/dist/gui/components/index.js +2 -0
  23. package/dist/gui/components/resizable.d.ts +7 -0
  24. package/dist/gui/components/resizable.js +7 -0
  25. package/dist/sdk/schemas/session.d.ts +390 -220
  26. package/dist/sdk/schemas/session.js +74 -29
  27. package/dist/sdk/transports/http.js +705 -472
  28. package/dist/sdk/transports/stdio.js +187 -32
  29. package/dist/tui/components/ChatView.js +19 -51
  30. package/dist/tui/components/MessageList.d.ts +2 -4
  31. package/dist/tui/components/MessageList.js +13 -37
  32. package/dist/tui/components/ToolCall.d.ts +9 -0
  33. package/dist/tui/components/ToolCall.js +41 -0
  34. package/dist/tui/components/ToolCallList.d.ts +8 -0
  35. package/dist/tui/components/ToolCallList.js +17 -0
  36. package/dist/tui/components/index.d.ts +2 -0
  37. package/dist/tui/components/index.js +2 -0
  38. package/package.json +4 -2
@@ -0,0 +1,174 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Tool call status lifecycle
4
+ */
5
+ export declare const ToolCallStatusSchema: z.ZodEnum<{
6
+ pending: "pending";
7
+ in_progress: "in_progress";
8
+ completed: "completed";
9
+ failed: "failed";
10
+ }>;
11
+ export type ToolCallStatus = z.infer<typeof ToolCallStatusSchema>;
12
+ /**
13
+ * Tool call categories for UI presentation
14
+ */
15
+ export declare const ToolCallKindSchema: z.ZodEnum<{
16
+ read: "read";
17
+ edit: "edit";
18
+ delete: "delete";
19
+ move: "move";
20
+ search: "search";
21
+ execute: "execute";
22
+ think: "think";
23
+ fetch: "fetch";
24
+ switch_mode: "switch_mode";
25
+ other: "other";
26
+ }>;
27
+ export type ToolCallKind = z.infer<typeof ToolCallKindSchema>;
28
+ /**
29
+ * File location with optional line number
30
+ */
31
+ export declare const FileLocationSchema: z.ZodObject<{
32
+ path: z.ZodString;
33
+ line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
34
+ }, z.core.$strip>;
35
+ export type FileLocation = z.infer<typeof FileLocationSchema>;
36
+ /**
37
+ * Token usage metadata for tracking LLM consumption
38
+ */
39
+ export declare const TokenUsageSchema: z.ZodObject<{
40
+ inputTokens: z.ZodOptional<z.ZodNumber>;
41
+ outputTokens: z.ZodOptional<z.ZodNumber>;
42
+ totalTokens: z.ZodOptional<z.ZodNumber>;
43
+ }, z.core.$strip>;
44
+ export type TokenUsage = z.infer<typeof TokenUsageSchema>;
45
+ /**
46
+ * Content block types for tool call results
47
+ */
48
+ export declare const ToolCallContentBlockSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
49
+ type: z.ZodLiteral<"content">;
50
+ content: z.ZodObject<{
51
+ type: z.ZodLiteral<"text">;
52
+ text: z.ZodString;
53
+ }, z.core.$strip>;
54
+ }, z.core.$strip>, z.ZodObject<{
55
+ type: z.ZodLiteral<"text">;
56
+ text: z.ZodString;
57
+ }, z.core.$strip>, z.ZodObject<{
58
+ type: z.ZodLiteral<"diff">;
59
+ path: z.ZodString;
60
+ oldText: z.ZodString;
61
+ newText: z.ZodString;
62
+ line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
63
+ }, z.core.$strip>, z.ZodObject<{
64
+ type: z.ZodLiteral<"terminal">;
65
+ terminalId: z.ZodString;
66
+ }, z.core.$strip>], "type">;
67
+ export type ToolCallContentBlock = z.infer<typeof ToolCallContentBlockSchema>;
68
+ /**
69
+ * Complete tool call state as displayed in the UI
70
+ */
71
+ export declare const ToolCallSchema: z.ZodObject<{
72
+ id: z.ZodString;
73
+ title: z.ZodString;
74
+ kind: z.ZodEnum<{
75
+ read: "read";
76
+ edit: "edit";
77
+ delete: "delete";
78
+ move: "move";
79
+ search: "search";
80
+ execute: "execute";
81
+ think: "think";
82
+ fetch: "fetch";
83
+ switch_mode: "switch_mode";
84
+ other: "other";
85
+ }>;
86
+ status: z.ZodEnum<{
87
+ pending: "pending";
88
+ in_progress: "in_progress";
89
+ completed: "completed";
90
+ failed: "failed";
91
+ }>;
92
+ locations: z.ZodOptional<z.ZodArray<z.ZodObject<{
93
+ path: z.ZodString;
94
+ line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
95
+ }, z.core.$strip>>>;
96
+ rawInput: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
97
+ rawOutput: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
98
+ content: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
99
+ type: z.ZodLiteral<"content">;
100
+ content: z.ZodObject<{
101
+ type: z.ZodLiteral<"text">;
102
+ text: z.ZodString;
103
+ }, z.core.$strip>;
104
+ }, z.core.$strip>, z.ZodObject<{
105
+ type: z.ZodLiteral<"text">;
106
+ text: z.ZodString;
107
+ }, z.core.$strip>, z.ZodObject<{
108
+ type: z.ZodLiteral<"diff">;
109
+ path: z.ZodString;
110
+ oldText: z.ZodString;
111
+ newText: z.ZodString;
112
+ line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
113
+ }, z.core.$strip>, z.ZodObject<{
114
+ type: z.ZodLiteral<"terminal">;
115
+ terminalId: z.ZodString;
116
+ }, z.core.$strip>], "type">>>;
117
+ error: z.ZodOptional<z.ZodString>;
118
+ startedAt: z.ZodOptional<z.ZodNumber>;
119
+ completedAt: z.ZodOptional<z.ZodNumber>;
120
+ tokenUsage: z.ZodOptional<z.ZodObject<{
121
+ inputTokens: z.ZodOptional<z.ZodNumber>;
122
+ outputTokens: z.ZodOptional<z.ZodNumber>;
123
+ totalTokens: z.ZodOptional<z.ZodNumber>;
124
+ }, z.core.$strip>>;
125
+ }, z.core.$strip>;
126
+ export type ToolCall = z.infer<typeof ToolCallSchema>;
127
+ /**
128
+ * Partial update for an existing tool call
129
+ */
130
+ export declare const ToolCallUpdateSchema: z.ZodObject<{
131
+ id: z.ZodString;
132
+ status: z.ZodOptional<z.ZodEnum<{
133
+ pending: "pending";
134
+ in_progress: "in_progress";
135
+ completed: "completed";
136
+ failed: "failed";
137
+ }>>;
138
+ locations: z.ZodOptional<z.ZodArray<z.ZodObject<{
139
+ path: z.ZodString;
140
+ line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
141
+ }, z.core.$strip>>>;
142
+ rawOutput: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
143
+ content: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
144
+ type: z.ZodLiteral<"content">;
145
+ content: z.ZodObject<{
146
+ type: z.ZodLiteral<"text">;
147
+ text: z.ZodString;
148
+ }, z.core.$strip>;
149
+ }, z.core.$strip>, z.ZodObject<{
150
+ type: z.ZodLiteral<"text">;
151
+ text: z.ZodString;
152
+ }, z.core.$strip>, z.ZodObject<{
153
+ type: z.ZodLiteral<"diff">;
154
+ path: z.ZodString;
155
+ oldText: z.ZodString;
156
+ newText: z.ZodString;
157
+ line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
158
+ }, z.core.$strip>, z.ZodObject<{
159
+ type: z.ZodLiteral<"terminal">;
160
+ terminalId: z.ZodString;
161
+ }, z.core.$strip>], "type">>>;
162
+ error: z.ZodOptional<z.ZodString>;
163
+ completedAt: z.ZodOptional<z.ZodNumber>;
164
+ tokenUsage: z.ZodOptional<z.ZodObject<{
165
+ inputTokens: z.ZodOptional<z.ZodNumber>;
166
+ outputTokens: z.ZodOptional<z.ZodNumber>;
167
+ totalTokens: z.ZodOptional<z.ZodNumber>;
168
+ }, z.core.$strip>>;
169
+ }, z.core.$strip>;
170
+ export type ToolCallUpdate = z.infer<typeof ToolCallUpdateSchema>;
171
+ /**
172
+ * Helper to merge a tool call update into an existing tool call
173
+ */
174
+ export declare function mergeToolCallUpdate(existing: ToolCall, update: ToolCallUpdate): ToolCall;
@@ -0,0 +1,130 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Tool call status lifecycle
4
+ */
5
+ export const ToolCallStatusSchema = z.enum([
6
+ "pending",
7
+ "in_progress",
8
+ "completed",
9
+ "failed",
10
+ ]);
11
+ /**
12
+ * Tool call categories for UI presentation
13
+ */
14
+ export const ToolCallKindSchema = z.enum([
15
+ "read",
16
+ "edit",
17
+ "delete",
18
+ "move",
19
+ "search",
20
+ "execute",
21
+ "think",
22
+ "fetch",
23
+ "switch_mode",
24
+ "other",
25
+ ]);
26
+ /**
27
+ * File location with optional line number
28
+ */
29
+ export const FileLocationSchema = z.object({
30
+ path: z.string(),
31
+ line: z.number().nullable().optional(),
32
+ });
33
+ /**
34
+ * Token usage metadata for tracking LLM consumption
35
+ */
36
+ export const TokenUsageSchema = z.object({
37
+ inputTokens: z.number().optional(),
38
+ outputTokens: z.number().optional(),
39
+ totalTokens: z.number().optional(),
40
+ });
41
+ /**
42
+ * Content block types for tool call results
43
+ */
44
+ export const ToolCallContentBlockSchema = z.discriminatedUnion("type", [
45
+ // ACP nested content format
46
+ z.object({
47
+ type: z.literal("content"),
48
+ content: z.object({
49
+ type: z.literal("text"),
50
+ text: z.string(),
51
+ }),
52
+ }),
53
+ // Direct text block (legacy)
54
+ z.object({
55
+ type: z.literal("text"),
56
+ text: z.string(),
57
+ }),
58
+ z.object({
59
+ type: z.literal("diff"),
60
+ path: z.string(),
61
+ oldText: z.string(),
62
+ newText: z.string(),
63
+ line: z.number().nullable().optional(),
64
+ }),
65
+ z.object({
66
+ type: z.literal("terminal"),
67
+ terminalId: z.string(),
68
+ }),
69
+ ]);
70
+ /**
71
+ * Complete tool call state as displayed in the UI
72
+ */
73
+ export const ToolCallSchema = z.object({
74
+ /** Unique identifier within the session */
75
+ id: z.string(),
76
+ /** Human-readable description of the operation */
77
+ title: z.string(),
78
+ /** Category for UI presentation */
79
+ kind: ToolCallKindSchema,
80
+ /** Current execution status */
81
+ status: ToolCallStatusSchema,
82
+ /** Affected file paths with optional line numbers */
83
+ locations: z.array(FileLocationSchema).optional(),
84
+ /** Raw parameters passed to the tool */
85
+ rawInput: z.record(z.string(), z.unknown()).optional(),
86
+ /** Raw response from the tool */
87
+ rawOutput: z.record(z.string(), z.unknown()).optional(),
88
+ /** Produced results (content blocks, diffs, terminal output) */
89
+ content: z.array(ToolCallContentBlockSchema).optional(),
90
+ /** Error message if status is 'failed' */
91
+ error: z.string().optional(),
92
+ /** Timestamp when the tool call started */
93
+ startedAt: z.number().optional(),
94
+ /** Timestamp when the tool call completed/failed */
95
+ completedAt: z.number().optional(),
96
+ /** Token usage metadata for this tool call */
97
+ tokenUsage: TokenUsageSchema.optional(),
98
+ });
99
+ /**
100
+ * Partial update for an existing tool call
101
+ */
102
+ export const ToolCallUpdateSchema = z.object({
103
+ id: z.string(),
104
+ status: ToolCallStatusSchema.optional(),
105
+ locations: z.array(FileLocationSchema).optional(),
106
+ rawOutput: z.record(z.string(), z.unknown()).optional(),
107
+ content: z.array(ToolCallContentBlockSchema).optional(),
108
+ error: z.string().optional(),
109
+ completedAt: z.number().optional(),
110
+ tokenUsage: TokenUsageSchema.optional(),
111
+ });
112
+ /**
113
+ * Helper to merge a tool call update into an existing tool call
114
+ */
115
+ export function mergeToolCallUpdate(existing, update) {
116
+ const merged = {
117
+ ...existing,
118
+ // Only update fields that are defined in the update
119
+ status: update.status ?? existing.status,
120
+ locations: update.locations ?? existing.locations,
121
+ rawOutput: update.rawOutput ?? existing.rawOutput,
122
+ content: update.content
123
+ ? [...(existing.content ?? []), ...update.content]
124
+ : existing.content,
125
+ error: update.error ?? existing.error,
126
+ completedAt: update.completedAt ?? existing.completedAt,
127
+ tokenUsage: update.tokenUsage ?? existing.tokenUsage,
128
+ };
129
+ return merged;
130
+ }
@@ -1,36 +1,36 @@
1
- import type {
2
- ConnectionStatus,
3
- DisplayMessage,
4
- InputState,
5
- } from "../schemas/index.js";
1
+ import type { ConnectionStatus, DisplayMessage, InputState } from "../schemas/index.js";
2
+ import type { ToolCall, ToolCallUpdate } from "../schemas/tool-call.js";
6
3
  /**
7
4
  * Chat store state
8
5
  */
9
6
  export interface ChatStore {
10
- connectionStatus: ConnectionStatus;
11
- sessionId: string | null;
12
- error: string | null;
13
- messages: DisplayMessage[];
14
- isStreaming: boolean;
15
- streamingStartTime: number | null;
16
- input: InputState;
17
- setConnectionStatus: (status: ConnectionStatus) => void;
18
- setSessionId: (id: string | null) => void;
19
- setError: (error: string | null) => void;
20
- addMessage: (message: DisplayMessage) => void;
21
- updateMessage: (id: string, updates: Partial<DisplayMessage>) => void;
22
- clearMessages: () => void;
23
- setIsStreaming: (streaming: boolean) => void;
24
- setStreamingStartTime: (time: number | null) => void;
25
- setInputValue: (value: string) => void;
26
- setInputSubmitting: (submitting: boolean) => void;
27
- addFileAttachment: (file: InputState["attachedFiles"][number]) => void;
28
- removeFileAttachment: (index: number) => void;
29
- clearInput: () => void;
7
+ connectionStatus: ConnectionStatus;
8
+ sessionId: string | null;
9
+ error: string | null;
10
+ messages: DisplayMessage[];
11
+ isStreaming: boolean;
12
+ streamingStartTime: number | null;
13
+ toolCalls: Record<string, ToolCall[]>;
14
+ input: InputState;
15
+ setConnectionStatus: (status: ConnectionStatus) => void;
16
+ setSessionId: (id: string | null) => void;
17
+ setError: (error: string | null) => void;
18
+ addMessage: (message: DisplayMessage) => void;
19
+ updateMessage: (id: string, updates: Partial<DisplayMessage>) => void;
20
+ clearMessages: () => void;
21
+ setIsStreaming: (streaming: boolean) => void;
22
+ setStreamingStartTime: (time: number | null) => void;
23
+ addToolCall: (sessionId: string, toolCall: ToolCall) => void;
24
+ updateToolCall: (sessionId: string, update: ToolCallUpdate) => void;
25
+ addToolCallToCurrentMessage: (toolCall: ToolCall) => void;
26
+ updateToolCallInCurrentMessage: (update: ToolCallUpdate) => void;
27
+ setInputValue: (value: string) => void;
28
+ setInputSubmitting: (submitting: boolean) => void;
29
+ addFileAttachment: (file: InputState["attachedFiles"][number]) => void;
30
+ removeFileAttachment: (index: number) => void;
31
+ clearInput: () => void;
30
32
  }
31
33
  /**
32
34
  * Create chat store
33
35
  */
34
- export declare const useChatStore: import("zustand").UseBoundStore<
35
- import("zustand").StoreApi<ChatStore>
36
- >;
36
+ export declare const useChatStore: import("zustand").UseBoundStore<import("zustand").StoreApi<ChatStore>>;
@@ -1,65 +1,129 @@
1
1
  import { create } from "zustand";
2
+ import { mergeToolCallUpdate } from "../schemas/tool-call.js";
2
3
  /**
3
4
  * Create chat store
4
5
  */
5
6
  export const useChatStore = create((set) => ({
6
- // Initial state
7
- connectionStatus: "disconnected",
8
- sessionId: null,
9
- error: null,
10
- messages: [],
11
- isStreaming: false,
12
- streamingStartTime: null,
13
- input: {
14
- value: "",
15
- isSubmitting: false,
16
- attachedFiles: [],
17
- },
18
- // Actions
19
- setConnectionStatus: (status) => set({ connectionStatus: status }),
20
- setSessionId: (id) => set({ sessionId: id }),
21
- setError: (error) => set({ error }),
22
- addMessage: (message) =>
23
- set((state) => ({
24
- messages: [...state.messages, message],
25
- })),
26
- updateMessage: (id, updates) =>
27
- set((state) => ({
28
- messages: state.messages.map((msg) =>
29
- msg.id === id ? { ...msg, ...updates } : msg,
30
- ),
31
- })),
32
- clearMessages: () => set({ messages: [] }),
33
- setIsStreaming: (streaming) => set({ isStreaming: streaming }),
34
- setStreamingStartTime: (time) => set({ streamingStartTime: time }),
35
- setInputValue: (value) =>
36
- set((state) => ({
37
- input: { ...state.input, value },
38
- })),
39
- setInputSubmitting: (submitting) =>
40
- set((state) => ({
41
- input: { ...state.input, isSubmitting: submitting },
42
- })),
43
- addFileAttachment: (file) =>
44
- set((state) => ({
45
- input: {
46
- ...state.input,
47
- attachedFiles: [...state.input.attachedFiles, file],
48
- },
49
- })),
50
- removeFileAttachment: (index) =>
51
- set((state) => ({
52
- input: {
53
- ...state.input,
54
- attachedFiles: state.input.attachedFiles.filter((_, i) => i !== index),
55
- },
56
- })),
57
- clearInput: () =>
58
- set((_state) => ({
59
- input: {
60
- value: "",
61
- isSubmitting: false,
62
- attachedFiles: [],
63
- },
64
- })),
7
+ // Initial state
8
+ connectionStatus: "disconnected",
9
+ sessionId: null,
10
+ error: null,
11
+ messages: [],
12
+ isStreaming: false,
13
+ streamingStartTime: null,
14
+ toolCalls: {},
15
+ input: {
16
+ value: "",
17
+ isSubmitting: false,
18
+ attachedFiles: [],
19
+ },
20
+ // Actions
21
+ setConnectionStatus: (status) => set({ connectionStatus: status }),
22
+ setSessionId: (id) => set({ sessionId: id }),
23
+ setError: (error) => set({ error }),
24
+ addMessage: (message) => set((state) => ({
25
+ messages: [...state.messages, message],
26
+ })),
27
+ updateMessage: (id, updates) => set((state) => ({
28
+ messages: state.messages.map((msg) => msg.id === id ? { ...msg, ...updates } : msg),
29
+ })),
30
+ clearMessages: () => set({ messages: [] }),
31
+ setIsStreaming: (streaming) => set({ isStreaming: streaming }),
32
+ setStreamingStartTime: (time) => set({ streamingStartTime: time }),
33
+ addToolCall: (sessionId, toolCall) => set((state) => ({
34
+ toolCalls: {
35
+ ...state.toolCalls,
36
+ [sessionId]: [...(state.toolCalls[sessionId] || []), toolCall],
37
+ },
38
+ })),
39
+ addToolCallToCurrentMessage: (toolCall) => set((state) => {
40
+ // Find the most recent assistant message (which should be streaming)
41
+ const lastAssistantIndex = state.messages.findLastIndex((msg) => msg.role === "assistant");
42
+ if (lastAssistantIndex === -1) {
43
+ console.warn("No assistant message found to add tool call to");
44
+ return state;
45
+ }
46
+ const messages = [...state.messages];
47
+ const lastAssistantMsg = messages[lastAssistantIndex];
48
+ if (!lastAssistantMsg)
49
+ return state;
50
+ messages[lastAssistantIndex] = {
51
+ ...lastAssistantMsg,
52
+ toolCalls: [...(lastAssistantMsg.toolCalls || []), toolCall],
53
+ };
54
+ return { messages };
55
+ }),
56
+ updateToolCallInCurrentMessage: (update) => set((state) => {
57
+ // Find the most recent assistant message
58
+ const lastAssistantIndex = state.messages.findLastIndex((msg) => msg.role === "assistant");
59
+ if (lastAssistantIndex === -1) {
60
+ console.warn("No assistant message found to update tool call in");
61
+ return state;
62
+ }
63
+ const messages = [...state.messages];
64
+ const lastAssistantMsg = messages[lastAssistantIndex];
65
+ if (!lastAssistantMsg)
66
+ return state;
67
+ const toolCalls = lastAssistantMsg.toolCalls || [];
68
+ const existingIndex = toolCalls.findIndex((tc) => tc.id === update.id);
69
+ if (existingIndex === -1) {
70
+ console.warn(`Tool call ${update.id} not found in message`);
71
+ return state;
72
+ }
73
+ const existing = toolCalls[existingIndex];
74
+ if (!existing)
75
+ return state;
76
+ const updatedToolCalls = [...toolCalls];
77
+ updatedToolCalls[existingIndex] = mergeToolCallUpdate(existing, update);
78
+ messages[lastAssistantIndex] = {
79
+ ...lastAssistantMsg,
80
+ toolCalls: updatedToolCalls,
81
+ };
82
+ return { messages };
83
+ }),
84
+ updateToolCall: (sessionId, update) => set((state) => {
85
+ const sessionToolCalls = state.toolCalls[sessionId] || [];
86
+ const existingIndex = sessionToolCalls.findIndex((tc) => tc.id === update.id);
87
+ if (existingIndex === -1) {
88
+ // Tool call not found, ignore update
89
+ return state;
90
+ }
91
+ const existing = sessionToolCalls[existingIndex];
92
+ if (!existing) {
93
+ return state;
94
+ }
95
+ const updatedToolCalls = [...sessionToolCalls];
96
+ updatedToolCalls[existingIndex] = mergeToolCallUpdate(existing, update);
97
+ return {
98
+ toolCalls: {
99
+ ...state.toolCalls,
100
+ [sessionId]: updatedToolCalls,
101
+ },
102
+ };
103
+ }),
104
+ setInputValue: (value) => set((state) => ({
105
+ input: { ...state.input, value },
106
+ })),
107
+ setInputSubmitting: (submitting) => set((state) => ({
108
+ input: { ...state.input, isSubmitting: submitting },
109
+ })),
110
+ addFileAttachment: (file) => set((state) => ({
111
+ input: {
112
+ ...state.input,
113
+ attachedFiles: [...state.input.attachedFiles, file],
114
+ },
115
+ })),
116
+ removeFileAttachment: (index) => set((state) => ({
117
+ input: {
118
+ ...state.input,
119
+ attachedFiles: state.input.attachedFiles.filter((_, i) => i !== index),
120
+ },
121
+ })),
122
+ clearInput: () => set((_state) => ({
123
+ input: {
124
+ value: "",
125
+ isSubmitting: false,
126
+ attachedFiles: [],
127
+ },
128
+ })),
65
129
  }));
@@ -1,7 +1,8 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { ArrowDown } from "lucide-react";
3
3
  import * as React from "react";
4
4
  import { cn } from "../lib/utils.js";
5
+ import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from "./resizable.js";
5
6
  import { Toaster } from "./Sonner.js";
6
7
  const ChatLayoutContext = React.createContext(undefined);
7
8
  const useChatLayoutContext = () => {
@@ -22,7 +23,7 @@ const ChatLayoutRoot = React.forwardRef(({ defaultSidebarOpen = false, defaultPa
22
23
  setPanelSize,
23
24
  activeTab,
24
25
  setActiveTab,
25
- }, children: _jsx("div", { ref: ref, className: cn("flex h-screen flex-row bg-background text-foreground", className), ...props, children: children }) }));
26
+ }, children: _jsx("div", { ref: ref, className: cn("flex h-screen flex-row bg-background text-foreground", className), ...props, children: _jsx(ResizablePanelGroup, { direction: "horizontal", className: "flex-1", children: children }) }) }));
26
27
  });
27
28
  ChatLayoutRoot.displayName = "ChatLayout.Root";
28
29
  const ChatLayoutHeader = React.forwardRef(({ className, children, ...props }, ref) => {
@@ -30,7 +31,7 @@ const ChatLayoutHeader = React.forwardRef(({ className, children, ...props }, re
30
31
  });
31
32
  ChatLayoutHeader.displayName = "ChatLayout.Header";
32
33
  const ChatLayoutMain = React.forwardRef(({ className, children, ...props }, ref) => {
33
- return (_jsx("div", { ref: ref, className: cn("flex flex-1 flex-col overflow-hidden", className), ...props, children: children }));
34
+ return (_jsx(ResizablePanel, { defaultSize: 75, minSize: 50, children: _jsx("div", { ref: ref, className: cn("flex flex-1 flex-col overflow-hidden h-full", className), ...props, children: children }) }));
34
35
  });
35
36
  ChatLayoutMain.displayName = "ChatLayout.Main";
36
37
  const ChatLayoutBody = React.forwardRef(({ showToaster = true, className, children, ...props }, ref) => {
@@ -90,13 +91,13 @@ const ChatLayoutAside = React.forwardRef(({ breakpoint = "lg", className, childr
90
91
  // Hidden state - don't render
91
92
  if (panelSize === "hidden")
92
93
  return null;
93
- return (_jsx("div", { ref: ref, className: cn(
94
- // Hidden by default, visible at breakpoint
95
- "hidden border-l border-border bg-card overflow-y-auto transition-all duration-300",
96
- // Breakpoint visibility
97
- breakpoint === "md" && "md:block", breakpoint === "lg" && "lg:block", breakpoint === "xl" && "xl:block", breakpoint === "2xl" && "2xl:block",
98
- // Size variants
99
- panelSize === "small" && "w-80", panelSize === "large" && "w-lg", className), ...props, children: children }));
94
+ return (_jsxs(_Fragment, { children: [_jsx(ResizableHandle, { withHandle: true }), _jsx(ResizablePanel, { defaultSize: 25, minSize: 15, maxSize: 50, children: _jsx("div", { ref: ref, className: cn(
95
+ // Hidden by default, visible at breakpoint
96
+ "hidden h-full border-l border-border bg-card overflow-y-auto transition-all duration-300",
97
+ // Breakpoint visibility
98
+ breakpoint === "md" && "md:block", breakpoint === "lg" && "lg:block", breakpoint === "xl" && "xl:block", breakpoint === "2xl" && "2xl:block",
99
+ // Size variants - width is now controlled by ResizablePanel
100
+ className), ...props, children: children }) })] }));
100
101
  });
101
102
  ChatLayoutAside.displayName = "ChatLayout.Aside";
102
103
  /* -------------------------------------------------------------------------------------------------
@@ -6,6 +6,7 @@ import { useChatStore } from "../../core/store/chat-store.js";
6
6
  import { cn } from "../lib/utils.js";
7
7
  import { Reasoning } from "./Reasoning.js";
8
8
  import { Response } from "./Response.js";
9
+ import { ToolCall } from "./ToolCall.js";
9
10
  /**
10
11
  * MessageContent component inspired by shadcn.io/ai
11
12
  * Provides the content container with role-based styling
@@ -97,7 +98,9 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
97
98
  const hasThinking = !!thinking;
98
99
  // Check if waiting (streaming but no content yet)
99
100
  const isWaiting = message.isStreaming && !message.content && message.role === "assistant";
100
- content = (_jsxs(_Fragment, { children: [message.role === "assistant" && hasThinking && (_jsx(Reasoning, { content: thinking, isStreaming: message.isStreaming, mode: thinkingDisplayStyle, autoCollapse: true })), isWaiting && streamingStartTime && (_jsxs("div", { className: "flex items-center gap-2 opacity-50", children: [_jsx(Loader2Icon, { className: "size-4 animate-spin text-muted-foreground" }), _jsx(WaitingElapsedTime, { startTime: streamingStartTime })] })), message.role === "user" ? (_jsx("div", { className: "whitespace-pre-wrap", children: message.content })) : (_jsx(Response, { content: message.content, isStreaming: message.isStreaming, showEmpty: false }))] }));
101
+ content = (_jsxs(_Fragment, { children: [message.role === "assistant" && hasThinking && (_jsx(Reasoning, { content: thinking, isStreaming: message.isStreaming, mode: thinkingDisplayStyle, autoCollapse: true })), isWaiting && streamingStartTime && (_jsxs("div", { className: "flex items-center gap-2 opacity-50", children: [_jsx(Loader2Icon, { className: "size-4 animate-spin text-muted-foreground" }), _jsx(WaitingElapsedTime, { startTime: streamingStartTime })] })), message.role === "assistant" &&
102
+ message.toolCalls &&
103
+ message.toolCalls.length > 0 && (_jsx("div", { className: "flex flex-col gap-2 mb-3", children: message.toolCalls.map((toolCall) => (_jsx(ToolCall, { toolCall: toolCall }, toolCall.id))) })), message.role === "user" ? (_jsx("div", { className: "whitespace-pre-wrap", children: message.content })) : (_jsx(Response, { content: message.content, isStreaming: message.isStreaming, showEmpty: false }))] }));
101
104
  }
102
105
  return (_jsx("div", { ref: ref, className: cn(messageContentVariants({ role, variant }), isStreaming && "animate-pulse-subtle", className), ...props, children: content }));
103
106
  });
@@ -0,0 +1,8 @@
1
+ import type { ToolCall as ToolCallType } from "../../core/schemas/tool-call.js";
2
+ export interface ToolCallProps {
3
+ toolCall: ToolCallType;
4
+ }
5
+ /**
6
+ * ToolCall component - displays a single tool call with collapsible details
7
+ */
8
+ export declare function ToolCall({ toolCall }: ToolCallProps): import("react/jsx-runtime").JSX.Element;