@townco/ui 0.1.22 → 0.1.24

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 (37) hide show
  1. package/dist/core/hooks/use-chat-input.js +5 -1
  2. package/dist/core/hooks/use-chat-messages.js +7 -3
  3. package/dist/core/hooks/use-chat-session.js +5 -1
  4. package/dist/core/index.d.ts +0 -1
  5. package/dist/core/index.js +0 -1
  6. package/dist/core/store/chat-store.d.ts +6 -0
  7. package/dist/core/store/chat-store.js +27 -13
  8. package/dist/gui/components/Button.d.ts +1 -1
  9. package/dist/gui/components/Button.js +5 -5
  10. package/dist/gui/components/ChatPanelTabContent.d.ts +5 -0
  11. package/dist/gui/components/ChatPanelTabContent.js +5 -0
  12. package/dist/gui/components/ChatSecondaryPanel.d.ts +1 -1
  13. package/dist/gui/components/ChatSecondaryPanel.js +8 -3
  14. package/dist/gui/components/Input.js +1 -1
  15. package/dist/gui/components/PanelTabsHeader.d.ts +1 -1
  16. package/dist/gui/components/PanelTabsHeader.js +6 -1
  17. package/dist/gui/components/SourceListItem.d.ts +15 -0
  18. package/dist/gui/components/SourceListItem.js +7 -0
  19. package/dist/gui/components/ToolCall.js +7 -7
  20. package/dist/gui/components/index.d.ts +2 -1
  21. package/dist/gui/components/index.js +3 -2
  22. package/dist/sdk/client/acp-client.js +11 -3
  23. package/dist/sdk/transports/http.js +1 -1
  24. package/dist/sdk/transports/stdio.js +1 -1
  25. package/dist/tui/components/ChatView.d.ts +3 -0
  26. package/dist/tui/components/ChatView.js +20 -8
  27. package/dist/tui/components/InputBox.js +22 -3
  28. package/dist/tui/components/MessageList.js +1 -1
  29. package/dist/tui/components/SimpleTextInput.d.ts +7 -0
  30. package/dist/tui/components/SimpleTextInput.js +208 -0
  31. package/dist/tui/components/StatusBar.js +3 -3
  32. package/dist/tui/components/index.d.ts +1 -0
  33. package/dist/tui/components/index.js +1 -0
  34. package/package.json +3 -3
  35. package/src/styles/global.css +138 -84
  36. package/dist/core/lib/logger.d.ts +0 -24
  37. package/dist/core/lib/logger.js +0 -108
@@ -6,18 +6,18 @@ import { MessageList } from "./MessageList.js";
6
6
  import { StatusBar } from "./StatusBar.js";
7
7
  export function ChatView({ client }) {
8
8
  const setIsStreaming = useChatStore((state) => state.setIsStreaming);
9
- const streamingStartTime = useChatStore((state) => state.streamingStartTime);
10
- const totalBilled = useChatStore((state) => state.totalBilled);
11
- const currentContext = useChatStore((state) => state.currentContext);
12
- const currentModel = useChatStore((state) => state.currentModel);
13
- const tokenDisplayMode = useChatStore((state) => state.tokenDisplayMode);
9
+ const _streamingStartTime = useChatStore((state) => state.streamingStartTime);
10
+ const _totalBilled = useChatStore((state) => state.totalBilled);
11
+ const _currentContext = useChatStore((state) => state.currentContext);
12
+ const _currentModel = useChatStore((state) => state.currentModel);
13
+ const _tokenDisplayMode = useChatStore((state) => state.tokenDisplayMode);
14
14
  // Use headless hooks for business logic
15
- const { connectionStatus, sessionId } = useChatSession(client);
15
+ useChatSession(client); // Subscribe to session changes
16
16
  const { messages, isStreaming } = useChatMessages(client);
17
17
  useToolCalls(client); // Still need to subscribe to tool call events
18
18
  const { value, isSubmitting, attachedFiles, onChange, onSubmit } = useChatInput(client);
19
19
  // Check if we're actively receiving content (hide waiting indicator)
20
- const hasStreamingContent = messages.some((msg) => msg.isStreaming && msg.content.length > 0);
20
+ const _hasStreamingContent = messages.some((msg) => msg.isStreaming && msg.content.length > 0);
21
21
  // Callbacks for keyboard shortcuts
22
22
  const handleEscape = () => {
23
23
  if (isStreaming) {
@@ -25,5 +25,17 @@ export function ChatView({ client }) {
25
25
  setIsStreaming(false);
26
26
  }
27
27
  };
28
- 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, totalBilled: totalBilled, currentContext: currentContext, currentModel: currentModel, tokenDisplayMode: tokenDisplayMode })] }));
28
+ 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 })] }));
29
+ }
30
+ // Export helper to render status content separately
31
+ export function ChatViewStatus({ client }) {
32
+ const streamingStartTime = useChatStore((state) => state.streamingStartTime);
33
+ const totalBilled = useChatStore((state) => state.totalBilled);
34
+ const currentContext = useChatStore((state) => state.currentContext);
35
+ const currentModel = useChatStore((state) => state.currentModel);
36
+ const tokenDisplayMode = useChatStore((state) => state.tokenDisplayMode);
37
+ const { connectionStatus, sessionId } = useChatSession(client);
38
+ const { messages, isStreaming } = useChatMessages(client);
39
+ const hasStreamingContent = messages.some((msg) => msg.isStreaming && msg.content.length > 0);
40
+ return (_jsx(StatusBar, { connectionStatus: connectionStatus, sessionId: sessionId, isStreaming: isStreaming, streamingStartTime: streamingStartTime, hasStreamingContent: hasStreamingContent, totalBilled: totalBilled, currentContext: currentContext, currentModel: currentModel, tokenDisplayMode: tokenDisplayMode }));
29
41
  }
@@ -1,10 +1,29 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text, useStdout } from "ink";
3
- import { ReadlineInput } from "./ReadlineInput.js";
2
+ import { Box, Text, useInput, useStdout } from "ink";
3
+ import { SimpleTextInput } from "./SimpleTextInput.js";
4
4
  export function InputBox({ value, isSubmitting, attachedFiles, onChange, onSubmit, onEscape, }) {
5
5
  const { stdout } = useStdout();
6
6
  const terminalWidth = stdout?.columns || 80;
7
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "blue", children: "─".repeat(terminalWidth) }), attachedFiles.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingTop: 1, children: [_jsx(Text, { dimColor: true, children: "Attached files:" }), attachedFiles.map((file) => (_jsxs(Box, { children: [_jsxs(Text, { color: "cyan", children: [" ", file.name] }), _jsxs(Text, { dimColor: true, children: [" (", formatFileSize(file.size), ")"] })] }, file.path)))] })), _jsxs(Box, { paddingY: 1, children: [_jsx(Text, { bold: true, color: "blue", children: "> " }), isSubmitting ? (_jsx(Text, { color: "gray", italic: true, children: "Sending..." })) : onEscape ? (_jsx(ReadlineInput, { value: value, onChange: onChange, onSubmit: onSubmit, onEscape: onEscape, placeholder: "Type your message..." })) : (_jsx(ReadlineInput, { value: value, onChange: onChange, onSubmit: onSubmit, placeholder: "Type your message..." }))] }), _jsx(Text, { color: "blue", children: "─".repeat(terminalWidth) })] }));
7
+ // Handle special keys for multi-line and escape
8
+ useInput((_input, key) => {
9
+ // Escape key
10
+ if (key.escape && onEscape) {
11
+ onEscape();
12
+ return;
13
+ }
14
+ // Don't interfere if submitting
15
+ if (isSubmitting)
16
+ return;
17
+ // Shift+Enter or Alt+Enter: insert newline
18
+ if (key.return && (key.shift || key.meta)) {
19
+ onChange(`${value}\n`);
20
+ return;
21
+ }
22
+ });
23
+ // Split value into lines for display
24
+ const lines = value.split("\n");
25
+ const _hasMultipleLines = lines.length > 1;
26
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "blue", children: "─".repeat(terminalWidth) }), attachedFiles.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Attached files:" }), attachedFiles.map((file) => (_jsxs(Box, { children: [_jsxs(Text, { color: "cyan", children: [" ", file.name] }), _jsxs(Text, { dimColor: true, children: [" (", formatFileSize(file.size), ")"] })] }, file.path)))] })), _jsx(Box, { paddingY: 1, flexDirection: "column", children: _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { bold: true, color: "blue", children: "> " }), _jsx(Box, { flexGrow: 1, children: isSubmitting ? (_jsx(Text, { color: "gray", italic: true, children: "Sending..." })) : (_jsx(SimpleTextInput, { value: value, onChange: onChange, onSubmit: onSubmit, placeholder: "Type your message... (\\ or Shift+Enter for newline)" })) })] }) })] }));
8
27
  }
9
28
  function formatFileSize(bytes) {
10
29
  if (bytes < 1024)
@@ -3,7 +3,7 @@ import { Box, Text } from "ink";
3
3
  import { GameOfLife } from "./GameOfLife.js";
4
4
  import { ToolCall } from "./ToolCall.js";
5
5
  export function MessageList({ messages }) {
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)))) }));
6
+ return (_jsx(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, height: "100%", children: messages.length === 0 ? (_jsx(GameOfLife, {})) : (messages.map((message) => (_jsx(Message, { message: message }, message.id)))) }));
7
7
  }
8
8
  function Message({ message }) {
9
9
  const roleColor = message.role === "user"
@@ -0,0 +1,7 @@
1
+ export interface SimpleTextInputProps {
2
+ value: string;
3
+ onChange: (value: string) => void;
4
+ onSubmit: () => void;
5
+ placeholder?: string;
6
+ }
7
+ export declare function SimpleTextInput({ value, onChange, onSubmit, placeholder, }: SimpleTextInputProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,208 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Text, useInput } from "ink";
3
+ import { useEffect, useState } from "react";
4
+ export function SimpleTextInput({ value, onChange, onSubmit, placeholder = "", }) {
5
+ const [cursorOffset, setCursorOffset] = useState(0);
6
+ useInput((input, key) => {
7
+ // Handle return/enter
8
+ if (key.return) {
9
+ onSubmit();
10
+ return;
11
+ }
12
+ // Handle backspace/delete
13
+ if (key.backspace || key.delete) {
14
+ const cursorPos = value.length + cursorOffset;
15
+ if (cursorPos > 0) {
16
+ const newValue = value.slice(0, cursorPos - 1) + value.slice(cursorPos);
17
+ onChange(newValue);
18
+ if (cursorOffset < 0) {
19
+ setCursorOffset(cursorOffset + 1);
20
+ }
21
+ }
22
+ return;
23
+ }
24
+ // Handle left arrow
25
+ if (key.leftArrow) {
26
+ const cursorPos = value.length + cursorOffset;
27
+ if (cursorPos > 0) {
28
+ setCursorOffset(cursorOffset - 1);
29
+ }
30
+ return;
31
+ }
32
+ // Handle right arrow
33
+ if (key.rightArrow) {
34
+ const cursorPos = value.length + cursorOffset;
35
+ if (cursorPos < value.length) {
36
+ setCursorOffset(cursorOffset + 1);
37
+ }
38
+ return;
39
+ }
40
+ // Handle up arrow - navigate to previous line
41
+ if (key.upArrow) {
42
+ const cursorPos = value.length + cursorOffset;
43
+ const lines = value.split("\n");
44
+ // Find current line and position within it
45
+ let currentLineIndex = 0;
46
+ let charCount = 0;
47
+ let posInLine = cursorPos;
48
+ for (let i = 0; i < lines.length; i++) {
49
+ const lineLength = (lines[i]?.length || 0) + (i < lines.length - 1 ? 1 : 0);
50
+ if (charCount + lineLength > cursorPos || i === lines.length - 1) {
51
+ currentLineIndex = i;
52
+ posInLine = cursorPos - charCount;
53
+ break;
54
+ }
55
+ charCount += lineLength;
56
+ }
57
+ // Move to previous line if possible
58
+ if (currentLineIndex > 0) {
59
+ const prevLine = lines[currentLineIndex - 1] || "";
60
+ const currentLine = lines[currentLineIndex] || "";
61
+ const prevLineStart = charCount - (currentLine.length + 1);
62
+ const targetPos = Math.min(posInLine, prevLine.length);
63
+ const newCursorPos = prevLineStart + targetPos;
64
+ setCursorOffset(newCursorPos - value.length);
65
+ }
66
+ return;
67
+ }
68
+ // Handle down arrow - navigate to next line
69
+ if (key.downArrow) {
70
+ const cursorPos = value.length + cursorOffset;
71
+ const lines = value.split("\n");
72
+ // Find current line and position within it
73
+ let currentLineIndex = 0;
74
+ let charCount = 0;
75
+ let posInLine = cursorPos;
76
+ for (let i = 0; i < lines.length; i++) {
77
+ const lineLength = (lines[i]?.length || 0) + (i < lines.length - 1 ? 1 : 0);
78
+ if (charCount + lineLength > cursorPos || i === lines.length - 1) {
79
+ currentLineIndex = i;
80
+ posInLine = cursorPos - charCount;
81
+ break;
82
+ }
83
+ charCount += lineLength;
84
+ }
85
+ // Move to next line if possible
86
+ if (currentLineIndex < lines.length - 1) {
87
+ const nextLine = lines[currentLineIndex + 1] || "";
88
+ const currentLine = lines[currentLineIndex] || "";
89
+ const nextLineStart = charCount + currentLine.length + 1;
90
+ const targetPos = Math.min(posInLine, nextLine.length);
91
+ const newCursorPos = nextLineStart + targetPos;
92
+ setCursorOffset(newCursorPos - value.length);
93
+ }
94
+ return;
95
+ }
96
+ // Handle Ctrl+A (move to beginning)
97
+ if (key.ctrl && input === "a") {
98
+ setCursorOffset(-value.length);
99
+ return;
100
+ }
101
+ // Handle Ctrl+E (move to end)
102
+ if (key.ctrl && input === "e") {
103
+ setCursorOffset(0);
104
+ return;
105
+ }
106
+ // Handle Ctrl+W (delete word backward)
107
+ if (key.ctrl && input === "w") {
108
+ const cursorPos = value.length + cursorOffset;
109
+ const before = value.slice(0, cursorPos);
110
+ const after = value.slice(cursorPos);
111
+ const match = before.match(/\s*\S*$/);
112
+ if (match) {
113
+ const newBefore = before.slice(0, -match[0].length);
114
+ onChange(newBefore + after);
115
+ setCursorOffset(-after.length);
116
+ }
117
+ return;
118
+ }
119
+ // Handle Ctrl+U (delete to beginning)
120
+ if (key.ctrl && input === "u") {
121
+ const cursorPos = value.length + cursorOffset;
122
+ onChange(value.slice(cursorPos));
123
+ setCursorOffset(-value.slice(cursorPos).length);
124
+ return;
125
+ }
126
+ // Handle Ctrl+K (delete to end)
127
+ if (key.ctrl && input === "k") {
128
+ const cursorPos = value.length + cursorOffset;
129
+ onChange(value.slice(0, cursorPos));
130
+ setCursorOffset(0);
131
+ return;
132
+ }
133
+ // Regular character input
134
+ if (!key.ctrl && !key.meta && input.length > 0) {
135
+ const cursorPos = value.length + cursorOffset;
136
+ const newValue = value.slice(0, cursorPos) + input + value.slice(cursorPos);
137
+ // If user types backslash at the end, automatically add newline
138
+ if (input === "\\" && cursorPos === value.length) {
139
+ onChange(`${newValue.slice(0, -1)}\n`);
140
+ // Reset cursor to end (no offset)
141
+ setCursorOffset(0);
142
+ }
143
+ else {
144
+ onChange(newValue);
145
+ // Keep cursor at same relative position
146
+ if (cursorOffset < 0) {
147
+ setCursorOffset(cursorOffset - input.length);
148
+ }
149
+ }
150
+ }
151
+ });
152
+ // Reset cursor when value changes externally (e.g., after submit)
153
+ useEffect(() => {
154
+ if (value.length === 0) {
155
+ setCursorOffset(0);
156
+ }
157
+ }, [value]);
158
+ // Display the input with cursor
159
+ const cursorPos = value.length + cursorOffset;
160
+ // Show placeholder if empty
161
+ if (value.length === 0) {
162
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { inverse: true, children: " " }), _jsx(Text, { dimColor: true, children: placeholder })] }));
163
+ }
164
+ // Split by newlines for multi-line rendering
165
+ const allLines = value.split("\n");
166
+ // Find which line the cursor is on and position within that line
167
+ let charCount = 0;
168
+ let cursorLineIndex = 0;
169
+ let cursorPosInLine = 0;
170
+ for (let i = 0; i < allLines.length; i++) {
171
+ const line = allLines[i] || "";
172
+ const lineEndPos = charCount + line.length;
173
+ // Check if cursor is within this line (including at the end before newline)
174
+ if (cursorPos <= lineEndPos) {
175
+ cursorLineIndex = i;
176
+ cursorPosInLine = cursorPos - charCount;
177
+ break;
178
+ }
179
+ // Move past the newline character for next iteration
180
+ charCount = lineEndPos + 1;
181
+ }
182
+ // Build the full display text with cursor
183
+ let displayText = "";
184
+ for (let i = 0; i < allLines.length; i++) {
185
+ const line = allLines[i] || "";
186
+ if (i === cursorLineIndex) {
187
+ // Add text before cursor
188
+ displayText += line.slice(0, cursorPosInLine);
189
+ // Mark cursor position - we'll add it separately
190
+ displayText += "\0"; // placeholder for cursor
191
+ // Add text after cursor
192
+ displayText += line.slice(cursorPosInLine + 1);
193
+ }
194
+ else {
195
+ displayText += line;
196
+ }
197
+ // Add newline if not last line
198
+ // if (i < allLines.length - 1) {
199
+ displayText += "\n";
200
+ // }
201
+ }
202
+ // Split by cursor placeholder
203
+ const parts = displayText.split("\0");
204
+ const beforeCursor = parts[0] || "";
205
+ const afterCursor = parts[1] || "";
206
+ const cursorChar = allLines[cursorLineIndex]?.[cursorPosInLine] || " ";
207
+ return (_jsxs(Text, { children: [beforeCursor, _jsx(Text, { inverse: true, children: cursorChar }), afterCursor] }));
208
+ }
@@ -1,5 +1,5 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Box, Text } from "ink";
2
+ import { Text } from "ink";
3
3
  import { useEffect, useState } from "react";
4
4
  import { calculateTokenPercentage, formatTokenPercentage, } from "../../core/utils/model-context.js";
5
5
  // Synonyms of "thinking" in multiple languages
@@ -113,7 +113,7 @@ export function StatusBar({ connectionStatus, isStreaming, streamingStartTime, h
113
113
  const contextPercentage = calculateTokenPercentage(contextTokens, currentModel ?? undefined);
114
114
  const contextPercentageStr = formatTokenPercentage(contextTokens, currentModel ?? undefined);
115
115
  const contextColor = getTokenColor(contextPercentage);
116
- return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { dimColor: true, children: "Context: " }), _jsx(Text, { color: contextColor, children: contextPercentageStr }), _jsx(Text, { dimColor: true, children: " | Input: " }), _jsx(Text, { children: formatTokenCount(inputTokens) }), _jsx(Text, { dimColor: true, children: " | Output: " }), _jsx(Text, { children: formatTokenCount(outputTokens) })] }));
116
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "Ctx: " }), _jsx(Text, { color: contextColor, children: contextPercentageStr }), _jsx(Text, { dimColor: true, children: " | In: " }), _jsx(Text, { children: formatTokenCount(inputTokens) }), _jsx(Text, { dimColor: true, children: " | Out: " }), _jsx(Text, { children: formatTokenCount(outputTokens) })] }));
117
117
  };
118
- return (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", paddingY: 1, children: [_jsxs(Box, { flexDirection: "row", gap: 2, children: [_jsx(Text, { dimColor: true, children: "Status: " }), _jsx(Text, { color: statusColor, children: connectionStatus }), showWaiting && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(WaitingElapsedTime, { startTime: streamingStartTime })] }))] }), renderTokenDisplay()] }));
118
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { color: statusColor, children: "\u25CF" }), _jsxs(Text, { children: [" ", connectionStatus] }), showWaiting && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(WaitingElapsedTime, { startTime: streamingStartTime })] })), _jsx(Text, { dimColor: true, children: " | " }), renderTokenDisplay()] }));
119
119
  }
@@ -7,6 +7,7 @@ export * from "./InputBox.js";
7
7
  export * from "./MessageList.js";
8
8
  export * from "./MultiSelect.js";
9
9
  export * from "./ReadlineInput.js";
10
+ export * from "./SimpleTextInput.js";
10
11
  export * from "./SingleSelect.js";
11
12
  export * from "./StatusBar.js";
12
13
  export * from "./ToolCall.js";
@@ -7,6 +7,7 @@ export * from "./InputBox.js";
7
7
  export * from "./MessageList.js";
8
8
  export * from "./MultiSelect.js";
9
9
  export * from "./ReadlineInput.js";
10
+ export * from "./SimpleTextInput.js";
10
11
  export * from "./SingleSelect.js";
11
12
  export * from "./StatusBar.js";
12
13
  export * from "./ToolCall.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@townco/ui",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -40,6 +40,7 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@agentclientprotocol/sdk": "^0.5.1",
43
+ "@townco/core": "0.0.2",
43
44
  "@radix-ui/react-dialog": "^1.1.15",
44
45
  "@radix-ui/react-dropdown-menu": "^2.1.16",
45
46
  "@radix-ui/react-label": "^2.1.8",
@@ -61,11 +62,10 @@
61
62
  },
62
63
  "devDependencies": {
63
64
  "@tailwindcss/postcss": "^4.1.17",
64
- "@townco/tsconfig": "0.1.19",
65
+ "@townco/tsconfig": "0.1.21",
65
66
  "@types/node": "^24.10.0",
66
67
  "@types/react": "^19.2.2",
67
68
  "ink": "^6.4.0",
68
- "ink-text-input": "^6.0.0",
69
69
  "react": "^19.2.0",
70
70
  "tailwindcss": "^4.1.17",
71
71
  "typescript": "^5.9.3"
@@ -2,92 +2,49 @@
2
2
 
3
3
  @source "../**/*.{ts,tsx}";
4
4
 
5
- :root {
6
- --background: 0 0% 100%;
7
- --foreground: 222.2 84% 4.9%;
8
- --card: 0 0% 100%;
9
- --card-foreground: 222.2 84% 4.9%;
10
- --popover: 0 0% 100%;
11
- --popover-foreground: 222.2 84% 4.9%;
12
- --primary: 222.2 47.4% 11.2%;
13
- --primary-foreground: 210 40% 98%;
14
- --secondary: 210 40% 96.1%;
15
- --secondary-foreground: 222.2 47.4% 11.2%;
16
- --muted: 210 40% 96.1%;
17
- --muted-foreground: 215.4 16.3% 46.9%;
18
- --accent: 210 40% 96.1%;
19
- --accent-foreground: 222.2 47.4% 11.2%;
20
- --destructive: 0 84.2% 60.2%;
21
- --destructive-foreground: 210 40% 98%;
22
- --border: 214.3 31.8% 91.4%;
23
- --input: 214.3 31.8% 91.4%;
24
- --ring: 222.2 84% 4.9%;
25
- --radius: 0.5rem;
5
+ @theme {
6
+ /* Semantic Color Tokens */
7
+ --color-background: var(--background);
8
+ --color-foreground: var(--foreground);
26
9
 
27
- /* Text colors from design system */
28
- --text-primary: 0 0% 9.02%; /* #171717 */
29
- --text-secondary: 0 0% 32.16%; /* #525252 */
30
- --text-tertiary: 0 0% 45.1%; /* #737373 */
31
- }
32
-
33
- .dark {
34
- --background: 222.2 84% 4.9%;
35
- --foreground: 210 40% 98%;
36
- --card: 222.2 84% 4.9%;
37
- --card-foreground: 210 40% 98%;
38
- --popover: 222.2 84% 4.9%;
39
- --popover-foreground: 210 40% 98%;
40
- --primary: 210 40% 98%;
41
- --primary-foreground: 222.2 47.4% 11.2%;
42
- --secondary: 217.2 32.6% 17.5%;
43
- --secondary-foreground: 210 40% 98%;
44
- --muted: 217.2 32.6% 17.5%;
45
- --muted-foreground: 215 20.2% 65.1%;
46
- --accent: 217.2 32.6% 17.5%;
47
- --accent-foreground: 210 40% 98%;
48
- --destructive: 0 62.8% 30.6%;
49
- --destructive-foreground: 210 40% 98%;
50
- --border: 217.2 32.6% 17.5%;
51
- --input: 217.2 32.6% 17.5%;
52
- --ring: 212.7 26.8% 83.9%;
10
+ --color-card: var(--card);
11
+ --color-card-foreground: var(--card-foreground);
53
12
 
54
- /* Text colors from design system (dark mode) */
55
- --text-primary: 0 0% 98%; /* Light text for dark mode */
56
- --text-secondary: 0 0% 70%; /* Muted light text */
57
- --text-tertiary: 0 0% 55%; /* More muted text */
58
- }
59
-
60
- @theme {
61
- /* Colors */
62
- --color-background: hsl(var(--background));
63
- --color-foreground: hsl(var(--foreground));
64
- --color-card: hsl(var(--card));
65
- --color-card-foreground: hsl(var(--card-foreground));
66
- --color-popover: hsl(var(--popover));
67
- --color-popover-foreground: hsl(var(--popover-foreground));
68
- --color-primary: hsl(var(--primary));
69
- --color-primary-foreground: hsl(var(--primary-foreground));
70
- --color-secondary: hsl(var(--secondary));
71
- --color-secondary-foreground: hsl(var(--secondary-foreground));
72
- --color-muted: hsl(var(--muted));
73
- --color-muted-foreground: hsl(var(--muted-foreground));
74
- --color-accent: hsl(var(--accent));
75
- --color-accent-foreground: hsl(var(--accent-foreground));
76
- --color-destructive: hsl(var(--destructive));
77
- --color-destructive-foreground: hsl(var(--destructive-foreground));
78
- --color-border: hsl(var(--border));
79
- --color-input: hsl(var(--input));
80
- --color-ring: hsl(var(--ring));
13
+ --color-popover: var(--popover);
14
+ --color-popover-foreground: var(--popover-foreground);
81
15
 
82
- /* Text colors from design system */
83
- --color-text-primary: hsl(var(--text-primary));
84
- --color-text-secondary: hsl(var(--text-secondary));
85
- --color-text-tertiary: hsl(var(--text-tertiary));
16
+ --color-primary: var(--primary);
17
+ --color-primary-foreground: var(--primary-foreground);
18
+ --color-primary-hover: var(--primary-hover);
86
19
 
87
- /* Shadows */
88
- --shadow-sm: 0 8px 24px -16px rgba(0, 0, 0, 0.04), 0 4px 16px 0 rgba(0, 0, 0, 0.04);
89
- --shadow-md: 0 8px 24px -16px rgba(0, 0, 0, 0.04), 0 4px 16px 0 rgba(0, 0, 0, 0.04);
20
+ --color-secondary: var(--secondary);
21
+ --color-secondary-foreground: var(--secondary-foreground);
22
+ --color-secondary-hover: var(--secondary-hover);
23
+
24
+ --color-muted: var(--muted);
25
+ --color-muted-foreground: var(--muted-foreground);
26
+
27
+ --color-accent: var(--accent);
28
+ --color-accent-foreground: var(--accent-foreground);
29
+ --color-accent-hover: var(--accent-hover);
30
+
31
+ --color-destructive: var(--destructive);
32
+ --color-destructive-foreground: var(--destructive-foreground);
33
+ --color-destructive-hover: var(--destructive-hover);
90
34
 
35
+ --color-border: var(--border);
36
+ --color-border-dark: var(--border-dark);
37
+ --color-border-focus: var(--border-focus);
38
+ --color-border-error: var(--border-error);
39
+ --color-input: var(--input);
40
+ --color-input-background: var(--input-background);
41
+ --color-ring: var(--ring);
42
+
43
+ /* Text colors from design system */
44
+ --color-text-primary: var(--text-primary);
45
+ --color-text-secondary: var(--text-secondary);
46
+ --color-text-tertiary: var(--text-tertiary);
47
+
91
48
  /* Layout widths - max-width utilities */
92
49
  --max-width-chat: 720px;
93
50
  --max-width-prose: 477px;
@@ -106,15 +63,112 @@
106
63
 
107
64
  --font-size-label: 0.875rem; /* 14px */
108
65
  --line-height-label: 1.5; /* 150% */
66
+
67
+ /* Shadows */
68
+ --shadow-sm: 0 8px 24px -16px rgba(0, 0, 0, 0.04), 0 4px 16px 0 rgba(0, 0, 0, 0.04);
69
+ --shadow-md: 0 8px 24px -16px rgba(0, 0, 0, 0.04), 0 4px 16px 0 rgba(0, 0, 0, 0.04);
70
+
71
+ /* Radius */
72
+ --radius: 0.5rem;
73
+ }
74
+
75
+ /*
76
+ Define the CSS variables that reference Tailwind palette colors.
77
+ This allows us to swap them in dark mode while keeping the semantic names.
78
+ */
79
+ :root {
80
+ --background: var(--color-white);
81
+ --foreground: var(--color-neutral-950);
82
+
83
+ --card: var(--color-white);
84
+ --card-foreground: var(--color-neutral-950);
85
+
86
+ --popover: var(--color-white);
87
+ --popover-foreground: var(--color-neutral-950);
88
+
89
+ --primary: var(--color-neutral-900);
90
+ --primary-foreground: var(--color-neutral-50);
91
+ --primary-hover: var(--color-neutral-700);
92
+
93
+ --secondary: var(--color-neutral-100);
94
+ --secondary-foreground: var(--color-neutral-900);
95
+ --secondary-hover: var(--color-neutral-50);
96
+
97
+ --muted: var(--color-neutral-50);
98
+ --muted-foreground: var(--color-neutral-500);
99
+
100
+ --accent: var(--color-neutral-100);
101
+ --accent-foreground: var(--color-neutral-900);
102
+ --accent-hover: var(--color-neutral-50);
103
+
104
+ --destructive: var(--color-red-500);
105
+ --destructive-foreground: var(--color-neutral-50);
106
+ --destructive-hover: var(--color-red-600);
107
+
108
+ --border: var(--color-neutral-200);
109
+ --border-dark: var(--color-neutral-300);
110
+ --border-focus: var(--color-neutral-400);
111
+ --border-error: var(--color-red-500);
112
+ --input: var(--color-neutral-200);
113
+ --input-background: var(--color-white);
114
+ --ring: var(--color-neutral-900);
115
+
116
+ /* Text colors from design system */
117
+ --text-primary: var(--color-neutral-900);
118
+ --text-secondary: var(--color-neutral-600);
119
+ --text-tertiary: var(--color-neutral-500);
120
+ }
121
+
122
+ .dark {
123
+ --background: var(--color-neutral-950);
124
+ --foreground: var(--color-neutral-50);
125
+
126
+ --card: var(--color-neutral-950);
127
+ --card-foreground: var(--color-neutral-50);
128
+
129
+ --popover: var(--color-neutral-950);
130
+ --popover-foreground: var(--color-neutral-50);
131
+
132
+ --primary: var(--color-neutral-50);
133
+ --primary-foreground: var(--color-neutral-900);
134
+ --primary-hover: var(--color-neutral-200);
135
+
136
+ --secondary: var(--color-neutral-800);
137
+ --secondary-foreground: var(--color-neutral-50);
138
+ --secondary-hover: var(--color-neutral-700);
139
+
140
+ --muted: var(--color-neutral-800);
141
+ --muted-foreground: var(--color-neutral-400);
142
+
143
+ --accent: var(--color-neutral-800);
144
+ --accent-foreground: var(--color-neutral-50);
145
+ --accent-hover: var(--color-neutral-700);
146
+
147
+ --destructive: var(--color-red-900);
148
+ --destructive-foreground: var(--color-neutral-50);
149
+ --destructive-hover: var(--color-red-800);
150
+
151
+ --border: var(--color-neutral-800);
152
+ --border-dark: var(--color-neutral-700);
153
+ --border-focus: var(--color-neutral-600);
154
+ --border-error: var(--color-red-500);
155
+ --input: var(--color-neutral-800);
156
+ --input-background: var(--color-neutral-950);
157
+ --ring: var(--color-neutral-300);
158
+
159
+ /* Text colors from design system (dark mode) */
160
+ --text-primary: var(--color-neutral-50);
161
+ --text-secondary: var(--color-neutral-400);
162
+ --text-tertiary: var(--color-neutral-500);
109
163
  }
110
164
 
111
165
  @layer base {
112
166
  * {
113
- border-color: hsl(var(--border));
167
+ border-color: var(--border);
114
168
  }
115
169
  body {
116
- background-color: hsl(var(--background));
117
- color: hsl(var(--foreground));
170
+ background-color: var(--background);
171
+ color: var(--foreground);
118
172
  }
119
173
  button {
120
174
  cursor: pointer;
@@ -1,24 +0,0 @@
1
- /**
2
- * Browser-compatible logger
3
- * Outputs structured JSON logs to console with color-coding
4
- */
5
- export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
6
- export declare class Logger {
7
- private service;
8
- private minLevel;
9
- constructor(service: string, minLevel?: LogLevel);
10
- private shouldLog;
11
- private log;
12
- trace(message: string, metadata?: Record<string, unknown>): void;
13
- debug(message: string, metadata?: Record<string, unknown>): void;
14
- info(message: string, metadata?: Record<string, unknown>): void;
15
- warn(message: string, metadata?: Record<string, unknown>): void;
16
- error(message: string, metadata?: Record<string, unknown>): void;
17
- fatal(message: string, metadata?: Record<string, unknown>): void;
18
- }
19
- /**
20
- * Create a logger instance for a service
21
- * @param service - Service name (e.g., "gui", "http-agent", "tui")
22
- * @param minLevel - Minimum log level to display (default: "debug")
23
- */
24
- export declare function createLogger(service: string, minLevel?: LogLevel): Logger;