@townco/ui 0.1.14 → 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 (42) 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/index.d.ts +1 -0
  8. package/dist/core/index.js +1 -0
  9. package/dist/core/lib/logger.d.ts +24 -0
  10. package/dist/core/lib/logger.js +108 -0
  11. package/dist/core/schemas/chat.d.ts +166 -83
  12. package/dist/core/schemas/chat.js +27 -27
  13. package/dist/core/schemas/index.d.ts +1 -0
  14. package/dist/core/schemas/index.js +1 -0
  15. package/dist/core/schemas/tool-call.d.ts +174 -0
  16. package/dist/core/schemas/tool-call.js +130 -0
  17. package/dist/core/store/chat-store.d.ts +28 -28
  18. package/dist/core/store/chat-store.js +123 -59
  19. package/dist/gui/components/ChatLayout.js +11 -10
  20. package/dist/gui/components/MessageContent.js +4 -1
  21. package/dist/gui/components/ToolCall.d.ts +8 -0
  22. package/dist/gui/components/ToolCall.js +100 -0
  23. package/dist/gui/components/ToolCallList.d.ts +9 -0
  24. package/dist/gui/components/ToolCallList.js +22 -0
  25. package/dist/gui/components/index.d.ts +2 -0
  26. package/dist/gui/components/index.js +2 -0
  27. package/dist/gui/components/resizable.d.ts +7 -0
  28. package/dist/gui/components/resizable.js +7 -0
  29. package/dist/sdk/schemas/session.d.ts +390 -220
  30. package/dist/sdk/schemas/session.js +74 -29
  31. package/dist/sdk/transports/http.js +705 -472
  32. package/dist/sdk/transports/stdio.js +187 -32
  33. package/dist/tui/components/ChatView.js +19 -51
  34. package/dist/tui/components/MessageList.d.ts +2 -4
  35. package/dist/tui/components/MessageList.js +13 -37
  36. package/dist/tui/components/ToolCall.d.ts +9 -0
  37. package/dist/tui/components/ToolCall.js +41 -0
  38. package/dist/tui/components/ToolCallList.d.ts +8 -0
  39. package/dist/tui/components/ToolCallList.js +17 -0
  40. package/dist/tui/components/index.d.ts +2 -0
  41. package/dist/tui/components/index.js +2 -0
  42. package/package.json +4 -2
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { ToolCallSchema } from "./tool-call.js";
2
3
  /**
3
4
  * Chat UI state schemas
4
5
  */
@@ -6,46 +7,45 @@ import { z } from "zod";
6
7
  * Display message schema (UI representation of messages)
7
8
  */
8
9
  export const DisplayMessage = z.object({
9
- id: z.string(),
10
- role: z.enum(["user", "assistant", "system"]),
11
- content: z.string(),
12
- timestamp: z.iso.datetime(),
13
- isStreaming: z.boolean().default(false),
14
- streamingStartTime: z.number().optional(), // Unix timestamp when streaming started
15
- metadata: z.record(z.string(), z.unknown()).optional(),
10
+ id: z.string(),
11
+ role: z.enum(["user", "assistant", "system"]),
12
+ content: z.string(),
13
+ timestamp: z.iso.datetime(),
14
+ isStreaming: z.boolean().default(false),
15
+ streamingStartTime: z.number().optional(), // Unix timestamp when streaming started
16
+ metadata: z.record(z.string(), z.unknown()).optional(),
17
+ toolCalls: z.array(ToolCallSchema).optional(),
16
18
  });
17
19
  /**
18
20
  * Input state schema
19
21
  */
20
22
  export const InputState = z.object({
21
- value: z.string(),
22
- isSubmitting: z.boolean(),
23
- attachedFiles: z.array(
24
- z.object({
25
- name: z.string(),
26
- path: z.string(),
27
- size: z.number(),
28
- mimeType: z.string(),
29
- }),
30
- ),
23
+ value: z.string(),
24
+ isSubmitting: z.boolean(),
25
+ attachedFiles: z.array(z.object({
26
+ name: z.string(),
27
+ path: z.string(),
28
+ size: z.number(),
29
+ mimeType: z.string(),
30
+ })),
31
31
  });
32
32
  /**
33
33
  * Chat session UI state
34
34
  */
35
35
  export const ChatSessionState = z.object({
36
- sessionId: z.string().nullable(),
37
- isConnected: z.boolean(),
38
- isStreaming: z.boolean(),
39
- messages: z.array(DisplayMessage),
40
- input: InputState,
41
- error: z.string().nullable(),
36
+ sessionId: z.string().nullable(),
37
+ isConnected: z.boolean(),
38
+ isStreaming: z.boolean(),
39
+ messages: z.array(DisplayMessage),
40
+ input: InputState,
41
+ error: z.string().nullable(),
42
42
  });
43
43
  /**
44
44
  * Connection status
45
45
  */
46
46
  export const ConnectionStatus = z.enum([
47
- "disconnected",
48
- "connecting",
49
- "connected",
50
- "error",
47
+ "disconnected",
48
+ "connecting",
49
+ "connected",
50
+ "error",
51
51
  ]);
@@ -2,3 +2,4 @@
2
2
  * Export all core schemas
3
3
  */
4
4
  export * from "./chat.js";
5
+ export * from "./tool-call.js";
@@ -2,3 +2,4 @@
2
2
  * Export all core schemas
3
3
  */
4
4
  export * from "./chat.js";
5
+ export * from "./tool-call.js";
@@ -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
  }));