@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
@@ -55,43 +55,198 @@ export class StdioTransport {
55
55
  // Handle session updates from the agent
56
56
  const paramsExtended = params;
57
57
  const update = paramsExtended.update;
58
- const sessionUpdate = {
59
- sessionId: self.currentSessionId || params.sessionId,
60
- status: "active",
61
- };
62
- // Queue message chunks if present - content is in update.content
63
- if (update?.content) {
58
+ const sessionId = self.currentSessionId || params.sessionId;
59
+ // Handle ACP tool call notifications
60
+ if (update?.sessionUpdate === "tool_call") {
61
+ // Initial tool call notification
62
+ const toolCall = {
63
+ id: update.toolCallId ?? "",
64
+ title: update.title ?? "",
65
+ kind: update.kind || "other",
66
+ status: update.status || "pending",
67
+ locations: update.locations,
68
+ rawInput: update.rawInput,
69
+ tokenUsage: update.tokenUsage,
70
+ content: update.content?.map((c) => {
71
+ // Type guard to safely check properties
72
+ if (typeof c !== "object" || c === null) {
73
+ return { type: "text", text: "" };
74
+ }
75
+ const content = c;
76
+ // Handle ACP nested content format
77
+ if (content.type === "content" &&
78
+ typeof content.content === "object" &&
79
+ content.content !== null) {
80
+ const innerContent = content.content;
81
+ if (innerContent.type === "text") {
82
+ return {
83
+ type: "content",
84
+ content: {
85
+ type: "text",
86
+ text: typeof innerContent.text === "string"
87
+ ? innerContent.text
88
+ : "",
89
+ },
90
+ };
91
+ }
92
+ }
93
+ // Handle legacy direct formats
94
+ if (content.type === "text")
95
+ return {
96
+ type: "text",
97
+ text: typeof content.text === "string" ? content.text : "",
98
+ };
99
+ if (content.type === "diff")
100
+ return {
101
+ type: "diff",
102
+ path: typeof content.path === "string" ? content.path : "",
103
+ oldText: typeof content.oldText === "string"
104
+ ? content.oldText
105
+ : "",
106
+ newText: typeof content.newText === "string"
107
+ ? content.newText
108
+ : "",
109
+ line: typeof content.line === "number"
110
+ ? content.line
111
+ : null,
112
+ };
113
+ if (content.type === "terminal")
114
+ return {
115
+ type: "terminal",
116
+ terminalId: typeof content.terminalId === "string"
117
+ ? content.terminalId
118
+ : "",
119
+ };
120
+ return { type: "text", text: "" };
121
+ }),
122
+ startedAt: Date.now(),
123
+ };
124
+ const sessionUpdate = {
125
+ type: "tool_call",
126
+ sessionId,
127
+ status: "active",
128
+ toolCall: toolCall,
129
+ };
130
+ self.notifySessionUpdate(sessionUpdate);
131
+ }
132
+ else if (update?.sessionUpdate === "tool_call_update") {
133
+ // Tool call update notification
134
+ const toolCallUpdate = {
135
+ id: update.toolCallId ?? "",
136
+ status: update.status,
137
+ locations: update.locations,
138
+ rawOutput: update.rawOutput,
139
+ tokenUsage: update.tokenUsage,
140
+ content: update.content?.map((c) => {
141
+ // Type guard to safely check properties
142
+ if (typeof c !== "object" || c === null) {
143
+ return { type: "text", text: "" };
144
+ }
145
+ const content = c;
146
+ // Handle ACP nested content format
147
+ if (content.type === "content" &&
148
+ typeof content.content === "object" &&
149
+ content.content !== null) {
150
+ const innerContent = content.content;
151
+ if (innerContent.type === "text") {
152
+ return {
153
+ type: "content",
154
+ content: {
155
+ type: "text",
156
+ text: typeof innerContent.text === "string"
157
+ ? innerContent.text
158
+ : "",
159
+ },
160
+ };
161
+ }
162
+ }
163
+ // Handle legacy direct formats
164
+ if (content.type === "text")
165
+ return {
166
+ type: "text",
167
+ text: typeof content.text === "string" ? content.text : "",
168
+ };
169
+ if (content.type === "diff")
170
+ return {
171
+ type: "diff",
172
+ path: typeof content.path === "string" ? content.path : "",
173
+ oldText: typeof content.oldText === "string"
174
+ ? content.oldText
175
+ : "",
176
+ newText: typeof content.newText === "string"
177
+ ? content.newText
178
+ : "",
179
+ line: typeof content.line === "number"
180
+ ? content.line
181
+ : null,
182
+ };
183
+ if (content.type === "terminal")
184
+ return {
185
+ type: "terminal",
186
+ terminalId: typeof content.terminalId === "string"
187
+ ? content.terminalId
188
+ : "",
189
+ };
190
+ return { type: "text", text: "" };
191
+ }),
192
+ error: update.error,
193
+ completedAt: update.status === "completed" || update.status === "failed"
194
+ ? Date.now()
195
+ : undefined,
196
+ };
197
+ const sessionUpdate = {
198
+ type: "tool_call_update",
199
+ sessionId,
200
+ status: "active",
201
+ toolCallUpdate: toolCallUpdate,
202
+ };
203
+ self.notifySessionUpdate(sessionUpdate);
204
+ }
205
+ else if (update?.sessionUpdate === "agent_message_chunk") {
206
+ // Handle agent message chunks
207
+ const sessionUpdate = {
208
+ type: "generic",
209
+ sessionId,
210
+ status: "active",
211
+ };
212
+ // Queue message chunks if present
213
+ // For agent_message_chunk, content is an object, not an array
64
214
  const content = update.content;
65
- let chunk = null;
66
- if (content.type === "text" && content.text) {
67
- chunk = {
68
- id: params.sessionId,
69
- role: "assistant",
70
- contentDelta: { type: "text", text: content.text },
71
- isComplete: false,
72
- };
73
- }
74
- else if (content.type === "tool_call") {
75
- chunk = {
76
- id: params.sessionId,
77
- role: "assistant",
78
- contentDelta: content,
79
- isComplete: false,
80
- };
81
- }
82
- if (chunk) {
83
- // Resolve any waiting receive() calls immediately
84
- const resolver = self.chunkResolvers.shift();
85
- if (resolver) {
86
- resolver(chunk);
215
+ if (content && typeof content === "object") {
216
+ const contentObj = content;
217
+ let chunk = null;
218
+ if (contentObj.type === "text" &&
219
+ typeof contentObj.text === "string") {
220
+ chunk = {
221
+ id: params.sessionId,
222
+ role: "assistant",
223
+ contentDelta: { type: "text", text: contentObj.text },
224
+ isComplete: false,
225
+ };
87
226
  }
88
- else {
89
- // Only queue if no resolver is waiting
90
- self.messageQueue.push(chunk);
227
+ if (chunk) {
228
+ // Resolve any waiting receive() calls immediately
229
+ const resolver = self.chunkResolvers.shift();
230
+ if (resolver) {
231
+ resolver(chunk);
232
+ }
233
+ else {
234
+ // Only queue if no resolver is waiting
235
+ self.messageQueue.push(chunk);
236
+ }
91
237
  }
92
238
  }
239
+ self.notifySessionUpdate(sessionUpdate);
240
+ }
241
+ else {
242
+ // Handle other session updates
243
+ const sessionUpdate = {
244
+ type: "generic",
245
+ sessionId,
246
+ status: "active",
247
+ };
248
+ self.notifySessionUpdate(sessionUpdate);
93
249
  }
94
- self.notifySessionUpdate(sessionUpdate);
95
250
  },
96
251
  async writeTextFile(params) {
97
252
  const fs = await import("node:fs/promises");
@@ -1,57 +1,25 @@
1
- import { Box } from "ink";
2
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import {
4
- useChatInput,
5
- useChatMessages,
6
- useChatSession,
7
- useChatStore,
8
- } from "../../core/index.js";
2
+ import { Box } from "ink";
3
+ import { useChatInput, useChatMessages, useChatSession, useChatStore, useToolCalls, } from "../../core/index.js";
9
4
  import { InputBox } from "./InputBox.js";
10
5
  import { MessageList } from "./MessageList.js";
11
6
  import { StatusBar } from "./StatusBar.js";
12
7
  export function ChatView({ client }) {
13
- const setIsStreaming = useChatStore((state) => state.setIsStreaming);
14
- const streamingStartTime = useChatStore((state) => state.streamingStartTime);
15
- // Use headless hooks for business logic
16
- const { connectionStatus, sessionId } = useChatSession(client);
17
- const { messages, isStreaming } = useChatMessages(client);
18
- const { value, isSubmitting, attachedFiles, onChange, onSubmit } =
19
- useChatInput(client);
20
- // Check if we're actively receiving content (hide waiting indicator)
21
- const hasStreamingContent = messages.some(
22
- (msg) => msg.isStreaming && msg.content.length > 0,
23
- );
24
- // Callbacks for keyboard shortcuts
25
- const handleEscape = () => {
26
- if (isStreaming) {
27
- // TODO: Implement proper cancellation when SDK supports it
28
- setIsStreaming(false);
29
- }
30
- };
31
- return _jsxs(Box, {
32
- flexDirection: "column",
33
- height: "100%",
34
- children: [
35
- _jsx(Box, {
36
- flexGrow: 1,
37
- flexDirection: "column",
38
- children: _jsx(MessageList, { messages: messages }),
39
- }),
40
- _jsx(InputBox, {
41
- value: value,
42
- isSubmitting: isSubmitting,
43
- attachedFiles: attachedFiles,
44
- onChange: onChange,
45
- onSubmit: onSubmit,
46
- onEscape: handleEscape,
47
- }),
48
- _jsx(StatusBar, {
49
- connectionStatus: connectionStatus,
50
- sessionId: sessionId,
51
- isStreaming: isStreaming,
52
- streamingStartTime: streamingStartTime,
53
- hasStreamingContent: hasStreamingContent,
54
- }),
55
- ],
56
- });
8
+ const setIsStreaming = useChatStore((state) => state.setIsStreaming);
9
+ const streamingStartTime = useChatStore((state) => state.streamingStartTime);
10
+ // Use headless hooks for business logic
11
+ const { connectionStatus, sessionId } = useChatSession(client);
12
+ const { messages, isStreaming } = useChatMessages(client);
13
+ useToolCalls(client); // Still need to subscribe to tool call events
14
+ const { value, isSubmitting, attachedFiles, onChange, onSubmit } = useChatInput(client);
15
+ // Check if we're actively receiving content (hide waiting indicator)
16
+ const hasStreamingContent = messages.some((msg) => msg.isStreaming && msg.content.length > 0);
17
+ // Callbacks for keyboard shortcuts
18
+ const handleEscape = () => {
19
+ if (isStreaming) {
20
+ // TODO: Implement proper cancellation when SDK supports it
21
+ setIsStreaming(false);
22
+ }
23
+ };
24
+ return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsx(Box, { flexGrow: 1, flexDirection: "column", children: _jsx(MessageList, { messages: messages }) }), _jsx(InputBox, { value: value, isSubmitting: isSubmitting, attachedFiles: attachedFiles, onChange: onChange, onSubmit: onSubmit, onEscape: handleEscape }), _jsx(StatusBar, { connectionStatus: connectionStatus, sessionId: sessionId, isStreaming: isStreaming, streamingStartTime: streamingStartTime, hasStreamingContent: hasStreamingContent })] }));
57
25
  }
@@ -1,7 +1,5 @@
1
1
  import type { DisplayMessage } from "../../core/index.js";
2
2
  export interface MessageListProps {
3
- messages: DisplayMessage[];
3
+ messages: DisplayMessage[];
4
4
  }
5
- export declare function MessageList({
6
- messages,
7
- }: MessageListProps): import("react/jsx-runtime").JSX.Element;
5
+ export declare function MessageList({ messages }: MessageListProps): import("react/jsx-runtime").JSX.Element;
@@ -1,43 +1,19 @@
1
- import { Box, Text } from "ink";
2
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
3
  import { GameOfLife } from "./GameOfLife.js";
4
+ import { ToolCall } from "./ToolCall.js";
4
5
  export function MessageList({ messages }) {
5
- return _jsx(Box, {
6
- flexDirection: "column",
7
- paddingX: 1,
8
- paddingY: 1,
9
- children:
10
- messages.length === 0
11
- ? _jsx(GameOfLife, {})
12
- : messages.map((message) =>
13
- _jsx(Message, { message: message }, message.id),
14
- ),
15
- });
6
+ return (_jsx(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: messages.length === 0 ? (_jsx(GameOfLife, {})) : (messages.map((message) => (_jsx(Message, { message: message }, message.id)))) }));
16
7
  }
17
8
  function Message({ message }) {
18
- const roleColor =
19
- message.role === "user"
20
- ? "blue"
21
- : message.role === "assistant"
22
- ? "green"
23
- : "gray";
24
- const roleSymbol =
25
- message.role === "user" ? ">" : message.role === "assistant" ? "⏺" : "•";
26
- const trimmedContent = message.content?.trim() || "";
27
- return _jsx(Box, {
28
- flexDirection: "column",
29
- marginY: 1,
30
- children: _jsxs(Box, {
31
- children: [
32
- _jsxs(Text, {
33
- bold: true,
34
- color: roleColor,
35
- children: [roleSymbol, " "],
36
- }),
37
- message.role === "user"
38
- ? _jsx(Text, { backgroundColor: "gray", children: trimmedContent })
39
- : _jsx(Text, { children: trimmedContent }),
40
- ],
41
- }),
42
- });
9
+ const roleColor = message.role === "user"
10
+ ? "blue"
11
+ : message.role === "assistant"
12
+ ? "green"
13
+ : "gray";
14
+ const roleSymbol = message.role === "user" ? ">" : message.role === "assistant" ? "⏺" : "•";
15
+ const trimmedContent = message.content?.trim() || "";
16
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [message.role === "assistant" &&
17
+ message.toolCalls &&
18
+ message.toolCalls.length > 0 && (_jsx(Box, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: message.toolCalls.map((toolCall) => (_jsx(ToolCall, { toolCall: toolCall }, toolCall.id))) })), _jsxs(Box, { children: [_jsxs(Text, { bold: true, color: roleColor, children: [roleSymbol, " "] }), message.role === "user" ? (_jsx(Text, { backgroundColor: "gray", children: trimmedContent })) : (_jsx(Text, { children: trimmedContent }))] })] }));
43
19
  }
@@ -0,0 +1,9 @@
1
+ import type { ToolCall as ToolCallType } from "../../core/schemas/tool-call.js";
2
+ export interface ToolCallProps {
3
+ toolCall: ToolCallType;
4
+ }
5
+ /**
6
+ * ToolCall component for TUI - displays a single tool call
7
+ * Always shows: tool name, start time, duration, and cost
8
+ */
9
+ export declare function ToolCall({ toolCall }: ToolCallProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ /**
4
+ * Tool call status indicators for terminal
5
+ */
6
+ const statusIndicators = {
7
+ pending: "⏳",
8
+ in_progress: "⚙️ ",
9
+ completed: "✓",
10
+ failed: "✗",
11
+ };
12
+ /**
13
+ * Tool call status colors
14
+ */
15
+ const statusColors = {
16
+ pending: "gray",
17
+ in_progress: "blue",
18
+ completed: "green",
19
+ failed: "red",
20
+ };
21
+ /**
22
+ * ToolCall component for TUI - displays a single tool call
23
+ * Always shows: tool name, start time, duration, and cost
24
+ */
25
+ export function ToolCall({ toolCall }) {
26
+ // Calculate duration if completed
27
+ const duration = toolCall.startedAt && toolCall.completedAt
28
+ ? ((toolCall.completedAt - toolCall.startedAt) / 1000).toFixed(1)
29
+ : null;
30
+ // Format start time
31
+ const startTime = toolCall.startedAt
32
+ ? new Date(toolCall.startedAt).toLocaleTimeString()
33
+ : "";
34
+ // Calculate cost (simplified - input tokens * rate + output tokens * rate)
35
+ // Using approximate rates: $3/M input, $15/M output for Claude
36
+ const cost = toolCall.tokenUsage?.inputTokens && toolCall.tokenUsage?.outputTokens
37
+ ? ((toolCall.tokenUsage.inputTokens / 1_000_000) * 3 +
38
+ (toolCall.tokenUsage.outputTokens / 1_000_000) * 15).toFixed(4)
39
+ : null;
40
+ return (_jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Text, { color: statusColors[toolCall.status], children: statusIndicators[toolCall.status] }), _jsx(Text, { color: "cyan", bold: true, children: "[TOOL]" }), _jsx(Text, { children: toolCall.title }), startTime && (_jsxs(Text, { color: "gray", dimColor: true, children: ["| ", startTime] })), duration && (_jsxs(Text, { color: "gray", dimColor: true, children: ["| ", duration, "s"] })), cost && (_jsxs(Text, { color: "gray", dimColor: true, children: ["| $", cost] }))] }));
41
+ }
@@ -0,0 +1,8 @@
1
+ import type { ToolCall as ToolCallType } from "../../core/schemas/tool-call.js";
2
+ export interface ToolCallListProps {
3
+ toolCalls: ToolCallType[];
4
+ }
5
+ /**
6
+ * ToolCallList component for TUI - renders a list of tool calls
7
+ */
8
+ export declare function ToolCallList({ toolCalls }: ToolCallListProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { ToolCall } from "./ToolCall.js";
4
+ /**
5
+ * ToolCallList component for TUI - renders a list of tool calls
6
+ */
7
+ export function ToolCallList({ toolCalls }) {
8
+ if (!toolCalls || toolCalls.length === 0) {
9
+ return (_jsx(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", paddingY: 2, children: _jsx(Text, { dimColor: true, children: "No tool calls yet" }) }));
10
+ }
11
+ // Group by status for better organization
12
+ const inProgress = toolCalls.filter((tc) => tc.status === "in_progress");
13
+ const pending = toolCalls.filter((tc) => tc.status === "pending");
14
+ const completed = toolCalls.filter((tc) => tc.status === "completed");
15
+ const failed = toolCalls.filter((tc) => tc.status === "failed");
16
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [inProgress.length > 0 && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { bold: true, color: "blue", children: ["\u25B6 In Progress (", inProgress.length, ")"] }), inProgress.map((tc) => (_jsx(ToolCall, { toolCall: tc }, tc.id)))] })), pending.length > 0 && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { bold: true, color: "gray", children: ["\u23F8 Pending (", pending.length, ")"] }), pending.map((tc) => (_jsx(ToolCall, { toolCall: tc }, tc.id)))] })), completed.length > 0 && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { bold: true, color: "green", children: ["\u2713 Completed (", completed.length, ")"] }), completed.slice(-5).map((tc) => (_jsx(ToolCall, { toolCall: tc }, tc.id))), completed.length > 5 && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["... and ", completed.length - 5, " more"] }) }))] })), failed.length > 0 && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { bold: true, color: "red", children: ["\u2717 Failed (", failed.length, ")"] }), failed.map((tc) => (_jsx(ToolCall, { toolCall: tc }, tc.id)))] }))] }));
17
+ }
@@ -9,3 +9,5 @@ export * from "./MultiSelect.js";
9
9
  export * from "./ReadlineInput.js";
10
10
  export * from "./SingleSelect.js";
11
11
  export * from "./StatusBar.js";
12
+ export * from "./ToolCall.js";
13
+ export * from "./ToolCallList.js";
@@ -9,3 +9,5 @@ export * from "./MultiSelect.js";
9
9
  export * from "./ReadlineInput.js";
10
10
  export * from "./SingleSelect.js";
11
11
  export * from "./StatusBar.js";
12
+ export * from "./ToolCall.js";
13
+ export * from "./ToolCallList.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@townco/ui",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -50,7 +50,9 @@
50
50
  "class-variance-authority": "^0.7.1",
51
51
  "clsx": "^2.1.1",
52
52
  "lucide-react": "^0.552.0",
53
+ "react-json-view": "^1.21.3",
53
54
  "react-markdown": "^10.1.0",
55
+ "react-resizable-panels": "^3.0.6",
54
56
  "remark-gfm": "^4.0.1",
55
57
  "sonner": "^2.0.7",
56
58
  "tailwind-merge": "^3.3.1",
@@ -59,7 +61,7 @@
59
61
  },
60
62
  "devDependencies": {
61
63
  "@tailwindcss/postcss": "^4.1.17",
62
- "@townco/tsconfig": "0.1.11",
64
+ "@townco/tsconfig": "0.1.13",
63
65
  "@types/node": "^24.10.0",
64
66
  "@types/react": "^19.2.2",
65
67
  "ink": "^6.4.0",