@townco/ui 0.1.22 → 0.1.23
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.
- package/dist/core/hooks/use-chat-input.js +5 -1
- package/dist/core/hooks/use-chat-messages.js +7 -3
- package/dist/core/hooks/use-chat-session.d.ts +1 -1
- package/dist/core/hooks/use-chat-session.js +5 -1
- package/dist/core/lib/logger.d.ts +35 -0
- package/dist/core/lib/logger.js +108 -25
- package/dist/core/store/chat-store.d.ts +6 -0
- package/dist/core/store/chat-store.js +21 -7
- package/dist/sdk/client/acp-client.js +11 -3
- package/dist/tui/components/ChatView.d.ts +3 -0
- package/dist/tui/components/ChatView.js +13 -1
- package/dist/tui/components/InputBox.js +23 -3
- package/dist/tui/components/LogsPanel.d.ts +5 -0
- package/dist/tui/components/LogsPanel.js +29 -0
- package/dist/tui/components/MessageList.js +1 -1
- package/dist/tui/components/SimpleTextInput.d.ts +7 -0
- package/dist/tui/components/SimpleTextInput.js +208 -0
- package/dist/tui/components/StatusBar.js +2 -2
- package/dist/tui/components/index.d.ts +1 -0
- package/dist/tui/components/index.js +1 -0
- package/package.json +2 -3
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
|
+
import { createLogger } from "../lib/logger.js";
|
|
2
3
|
import { useChatStore } from "../store/chat-store.js";
|
|
3
4
|
import { useChatMessages } from "./use-chat-messages.js";
|
|
5
|
+
const logger = createLogger("use-chat-input", "debug");
|
|
4
6
|
/**
|
|
5
7
|
* Hook for managing chat input
|
|
6
8
|
*/
|
|
@@ -33,7 +35,9 @@ export function useChatInput(client) {
|
|
|
33
35
|
}
|
|
34
36
|
catch (error) {
|
|
35
37
|
// Error is handled in useChatMessages
|
|
36
|
-
|
|
38
|
+
logger.error("Failed to send message", {
|
|
39
|
+
error: error instanceof Error ? error.message : String(error),
|
|
40
|
+
});
|
|
37
41
|
}
|
|
38
42
|
finally {
|
|
39
43
|
setInputSubmitting(false);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
|
+
import { createLogger } from "../lib/logger.js";
|
|
2
3
|
import { useChatStore } from "../store/chat-store.js";
|
|
4
|
+
const logger = createLogger("use-chat-messages", "debug");
|
|
3
5
|
/**
|
|
4
6
|
* Hook for managing chat messages
|
|
5
7
|
*/
|
|
@@ -17,12 +19,12 @@ export function useChatMessages(client) {
|
|
|
17
19
|
*/
|
|
18
20
|
const sendMessage = useCallback(async (content) => {
|
|
19
21
|
if (!client) {
|
|
20
|
-
|
|
22
|
+
logger.error("No client available");
|
|
21
23
|
setError("No client available");
|
|
22
24
|
return;
|
|
23
25
|
}
|
|
24
26
|
if (!sessionId) {
|
|
25
|
-
|
|
27
|
+
logger.error("No active session");
|
|
26
28
|
setError("No active session");
|
|
27
29
|
return;
|
|
28
30
|
}
|
|
@@ -74,7 +76,9 @@ export function useChatMessages(client) {
|
|
|
74
76
|
let accumulatedContent = "";
|
|
75
77
|
for await (const chunk of messageStream) {
|
|
76
78
|
if (chunk.tokenUsage) {
|
|
77
|
-
|
|
79
|
+
logger.debug("chunk.tokenUsage", {
|
|
80
|
+
tokenUsage: chunk.tokenUsage,
|
|
81
|
+
});
|
|
78
82
|
}
|
|
79
83
|
if (chunk.isComplete) {
|
|
80
84
|
// Update final message
|
|
@@ -3,7 +3,7 @@ import type { AcpClient } from "../../sdk/client/index.js";
|
|
|
3
3
|
* Hook for managing chat session lifecycle
|
|
4
4
|
*/
|
|
5
5
|
export declare function useChatSession(client: AcpClient | null): {
|
|
6
|
-
connectionStatus: "
|
|
6
|
+
connectionStatus: "disconnected" | "connecting" | "connected" | "error";
|
|
7
7
|
sessionId: string | null;
|
|
8
8
|
connect: () => Promise<void>;
|
|
9
9
|
startSession: () => Promise<void>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect } from "react";
|
|
2
|
+
import { createLogger } from "../lib/logger.js";
|
|
2
3
|
import { useChatStore } from "../store/chat-store.js";
|
|
4
|
+
const logger = createLogger("use-chat-session", "debug");
|
|
3
5
|
/**
|
|
4
6
|
* Hook for managing chat session lifecycle
|
|
5
7
|
*/
|
|
@@ -26,7 +28,9 @@ export function useChatSession(client) {
|
|
|
26
28
|
setConnectionStatus("connected");
|
|
27
29
|
}
|
|
28
30
|
catch (error) {
|
|
29
|
-
|
|
31
|
+
logger.error("Failed to connect", {
|
|
32
|
+
error: error instanceof Error ? error.message : String(error),
|
|
33
|
+
});
|
|
30
34
|
const message = error instanceof Error ? error.message : String(error);
|
|
31
35
|
setError(message);
|
|
32
36
|
setConnectionStatus("error");
|
|
@@ -1,12 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Browser-compatible logger
|
|
3
3
|
* Outputs structured JSON logs to console with color-coding
|
|
4
|
+
* Also captures logs to a global store for in-app viewing
|
|
5
|
+
* In Node.js environment with logsDir option, also writes to .logs/ directory
|
|
4
6
|
*/
|
|
5
7
|
export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
|
|
8
|
+
export interface LogEntry {
|
|
9
|
+
id: string;
|
|
10
|
+
timestamp: string;
|
|
11
|
+
level: LogLevel;
|
|
12
|
+
service: string;
|
|
13
|
+
message: string;
|
|
14
|
+
metadata?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Get all captured logs
|
|
18
|
+
*/
|
|
19
|
+
export declare function getCapturedLogs(): LogEntry[];
|
|
20
|
+
/**
|
|
21
|
+
* Clear all captured logs
|
|
22
|
+
*/
|
|
23
|
+
export declare function clearCapturedLogs(): void;
|
|
24
|
+
/**
|
|
25
|
+
* Subscribe to log updates
|
|
26
|
+
*/
|
|
27
|
+
type LogSubscriber = (entry: LogEntry) => void;
|
|
28
|
+
export declare function subscribeToLogs(callback: LogSubscriber): () => void;
|
|
29
|
+
/**
|
|
30
|
+
* Configure global logs directory for file writing
|
|
31
|
+
* Must be called before creating any loggers (typically at TUI startup)
|
|
32
|
+
*/
|
|
33
|
+
export declare function configureLogsDir(logsDir: string): void;
|
|
6
34
|
export declare class Logger {
|
|
7
35
|
private service;
|
|
8
36
|
private minLevel;
|
|
37
|
+
private logFilePath?;
|
|
38
|
+
private logsDir?;
|
|
39
|
+
private writeQueue;
|
|
40
|
+
private isWriting;
|
|
9
41
|
constructor(service: string, minLevel?: LogLevel);
|
|
42
|
+
private setupFileLogging;
|
|
43
|
+
private writeToFile;
|
|
10
44
|
private shouldLog;
|
|
11
45
|
private log;
|
|
12
46
|
trace(message: string, metadata?: Record<string, unknown>): void;
|
|
@@ -22,3 +56,4 @@ export declare class Logger {
|
|
|
22
56
|
* @param minLevel - Minimum log level to display (default: "debug")
|
|
23
57
|
*/
|
|
24
58
|
export declare function createLogger(service: string, minLevel?: LogLevel): Logger;
|
|
59
|
+
export {};
|
package/dist/core/lib/logger.js
CHANGED
|
@@ -1,7 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Browser-compatible logger
|
|
3
3
|
* Outputs structured JSON logs to console with color-coding
|
|
4
|
+
* Also captures logs to a global store for in-app viewing
|
|
5
|
+
* In Node.js environment with logsDir option, also writes to .logs/ directory
|
|
4
6
|
*/
|
|
7
|
+
// Check if running in Node.js
|
|
8
|
+
const isNode = typeof process !== "undefined" && process.versions?.node;
|
|
9
|
+
// Global logs directory configuration (set once at app startup for TUI)
|
|
10
|
+
let globalLogsDir;
|
|
11
|
+
// Global log store
|
|
12
|
+
const globalLogStore = [];
|
|
13
|
+
let logIdCounter = 0;
|
|
14
|
+
/**
|
|
15
|
+
* Get all captured logs
|
|
16
|
+
*/
|
|
17
|
+
export function getCapturedLogs() {
|
|
18
|
+
return [...globalLogStore];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Clear all captured logs
|
|
22
|
+
*/
|
|
23
|
+
export function clearCapturedLogs() {
|
|
24
|
+
globalLogStore.length = 0;
|
|
25
|
+
}
|
|
26
|
+
const logSubscribers = new Set();
|
|
27
|
+
export function subscribeToLogs(callback) {
|
|
28
|
+
logSubscribers.add(callback);
|
|
29
|
+
return () => logSubscribers.delete(callback);
|
|
30
|
+
}
|
|
31
|
+
function notifyLogSubscribers(entry) {
|
|
32
|
+
for (const callback of logSubscribers) {
|
|
33
|
+
callback(entry);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Configure global logs directory for file writing
|
|
38
|
+
* Must be called before creating any loggers (typically at TUI startup)
|
|
39
|
+
*/
|
|
40
|
+
export function configureLogsDir(logsDir) {
|
|
41
|
+
globalLogsDir = logsDir;
|
|
42
|
+
}
|
|
5
43
|
const LOG_LEVELS = {
|
|
6
44
|
trace: 0,
|
|
7
45
|
debug: 1,
|
|
@@ -18,7 +56,7 @@ const _LOG_COLORS = {
|
|
|
18
56
|
error: "#EF4444", // red
|
|
19
57
|
fatal: "#DC2626", // dark red
|
|
20
58
|
};
|
|
21
|
-
const
|
|
59
|
+
const _LOG_STYLES = {
|
|
22
60
|
trace: "color: #6B7280",
|
|
23
61
|
debug: "color: #3B82F6; font-weight: bold",
|
|
24
62
|
info: "color: #10B981; font-weight: bold",
|
|
@@ -29,6 +67,10 @@ const LOG_STYLES = {
|
|
|
29
67
|
export class Logger {
|
|
30
68
|
service;
|
|
31
69
|
minLevel;
|
|
70
|
+
logFilePath;
|
|
71
|
+
logsDir;
|
|
72
|
+
writeQueue = [];
|
|
73
|
+
isWriting = false;
|
|
32
74
|
constructor(service, minLevel = "debug") {
|
|
33
75
|
this.service = service;
|
|
34
76
|
this.minLevel = minLevel;
|
|
@@ -37,6 +79,48 @@ export class Logger {
|
|
|
37
79
|
process.env?.NODE_ENV === "production") {
|
|
38
80
|
this.minLevel = "info";
|
|
39
81
|
}
|
|
82
|
+
// Note: File logging setup is done lazily in log() method
|
|
83
|
+
// This allows loggers created before configureLogsDir() to still write to files
|
|
84
|
+
}
|
|
85
|
+
setupFileLogging() {
|
|
86
|
+
if (!isNode || !globalLogsDir)
|
|
87
|
+
return;
|
|
88
|
+
try {
|
|
89
|
+
// Dynamic import for Node.js modules
|
|
90
|
+
const path = require("node:path");
|
|
91
|
+
const fs = require("node:fs");
|
|
92
|
+
this.logsDir = globalLogsDir;
|
|
93
|
+
this.logFilePath = path.join(this.logsDir, `${this.service}.log`);
|
|
94
|
+
// Create logs directory if it doesn't exist
|
|
95
|
+
if (!fs.existsSync(this.logsDir)) {
|
|
96
|
+
fs.mkdirSync(this.logsDir, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (_error) {
|
|
100
|
+
// Silently fail if we can't set up file logging
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async writeToFile(content) {
|
|
104
|
+
if (!this.logFilePath || !isNode)
|
|
105
|
+
return;
|
|
106
|
+
this.writeQueue.push(content);
|
|
107
|
+
if (this.isWriting) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
this.isWriting = true;
|
|
111
|
+
while (this.writeQueue.length > 0) {
|
|
112
|
+
const batch = this.writeQueue.splice(0, this.writeQueue.length);
|
|
113
|
+
const data = `${batch.join("\n")}\n`;
|
|
114
|
+
try {
|
|
115
|
+
// Dynamic import for Node.js modules
|
|
116
|
+
const fs = require("node:fs");
|
|
117
|
+
await fs.promises.appendFile(this.logFilePath, data, "utf-8");
|
|
118
|
+
}
|
|
119
|
+
catch (_error) {
|
|
120
|
+
// Silently fail
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
this.isWriting = false;
|
|
40
124
|
}
|
|
41
125
|
shouldLog(level) {
|
|
42
126
|
return LOG_LEVELS[level] >= LOG_LEVELS[this.minLevel];
|
|
@@ -46,38 +130,37 @@ export class Logger {
|
|
|
46
130
|
return;
|
|
47
131
|
}
|
|
48
132
|
const entry = {
|
|
133
|
+
id: `log_${++logIdCounter}`,
|
|
49
134
|
timestamp: new Date().toISOString(),
|
|
50
135
|
level,
|
|
51
136
|
service: this.service,
|
|
52
137
|
message,
|
|
53
138
|
...(metadata && { metadata }),
|
|
54
139
|
};
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
case "trace":
|
|
64
|
-
case "debug":
|
|
65
|
-
case "info":
|
|
66
|
-
console.log(prefix, style, msg, ...(metadata ? [metadata] : []));
|
|
67
|
-
break;
|
|
68
|
-
case "warn":
|
|
69
|
-
console.warn(prefix, style, msg, ...(metadata ? [metadata] : []));
|
|
70
|
-
break;
|
|
71
|
-
case "error":
|
|
72
|
-
case "fatal":
|
|
73
|
-
console.error(prefix, style, msg, ...(metadata ? [metadata] : []));
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
140
|
+
// Store in global log store
|
|
141
|
+
globalLogStore.push(entry);
|
|
142
|
+
// Notify subscribers
|
|
143
|
+
notifyLogSubscribers(entry);
|
|
144
|
+
// Write to file in Node.js (for logs tab to read)
|
|
145
|
+
// Lazily set up file logging if globalLogsDir was configured after this logger was created
|
|
146
|
+
if (isNode && !this.logFilePath && globalLogsDir) {
|
|
147
|
+
this.setupFileLogging();
|
|
76
148
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
149
|
+
if (isNode && this.logFilePath) {
|
|
150
|
+
// Write as JSON without the id field (to match expected format)
|
|
151
|
+
const fileEntry = {
|
|
152
|
+
timestamp: entry.timestamp,
|
|
153
|
+
level: entry.level,
|
|
154
|
+
service: entry.service,
|
|
155
|
+
message: entry.message,
|
|
156
|
+
...(entry.metadata && { metadata: entry.metadata }),
|
|
157
|
+
};
|
|
158
|
+
this.writeToFile(JSON.stringify(fileEntry)).catch(() => {
|
|
159
|
+
// Silently fail
|
|
160
|
+
});
|
|
80
161
|
}
|
|
162
|
+
// No console output - logs are only captured and displayed in UI
|
|
163
|
+
// This prevents logs from polluting stdout/stderr in TUI mode
|
|
81
164
|
}
|
|
82
165
|
trace(message, metadata) {
|
|
83
166
|
this.log("trace", message, metadata);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type LogEntry } from "../lib/logger.js";
|
|
1
2
|
import type { ConnectionStatus, DisplayMessage, InputState } from "../schemas/index.js";
|
|
2
3
|
import type { ToolCall, ToolCallUpdate } from "../schemas/tool-call.js";
|
|
3
4
|
/**
|
|
@@ -23,6 +24,8 @@ export interface ChatStore {
|
|
|
23
24
|
};
|
|
24
25
|
currentModel: string | null;
|
|
25
26
|
tokenDisplayMode: "context" | "input" | "output";
|
|
27
|
+
logs: LogEntry[];
|
|
28
|
+
activeTab: "chat" | "logs";
|
|
26
29
|
input: InputState;
|
|
27
30
|
setConnectionStatus: (status: ConnectionStatus) => void;
|
|
28
31
|
setSessionId: (id: string | null) => void;
|
|
@@ -49,6 +52,9 @@ export interface ChatStore {
|
|
|
49
52
|
setCurrentModel: (model: string) => void;
|
|
50
53
|
resetTokens: () => void;
|
|
51
54
|
cycleTokenDisplayMode: () => void;
|
|
55
|
+
addLog: (log: LogEntry) => void;
|
|
56
|
+
clearLogs: () => void;
|
|
57
|
+
setActiveTab: (tab: "chat" | "logs") => void;
|
|
52
58
|
}
|
|
53
59
|
/**
|
|
54
60
|
* Create chat store
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { create } from "zustand";
|
|
2
|
+
import { createLogger } from "../lib/logger.js";
|
|
2
3
|
import { mergeToolCallUpdate } from "../schemas/tool-call.js";
|
|
4
|
+
const logger = createLogger("chat-store", "debug");
|
|
3
5
|
/**
|
|
4
6
|
* Create chat store
|
|
5
7
|
*/
|
|
@@ -24,6 +26,8 @@ export const useChatStore = create((set) => ({
|
|
|
24
26
|
},
|
|
25
27
|
currentModel: "claude-sonnet-4-5-20250929", // Default model, TODO: get from server
|
|
26
28
|
tokenDisplayMode: "context", // Default to showing context (both billed and current)
|
|
29
|
+
logs: [],
|
|
30
|
+
activeTab: "chat",
|
|
27
31
|
input: {
|
|
28
32
|
value: "",
|
|
29
33
|
isSubmitting: false,
|
|
@@ -46,7 +50,10 @@ export const useChatStore = create((set) => ({
|
|
|
46
50
|
let finalUpdates = updates;
|
|
47
51
|
if (updates.tokenUsage) {
|
|
48
52
|
const existingTokenUsage = existingMessage?.tokenUsage;
|
|
49
|
-
|
|
53
|
+
logger.debug("updateMessage: incoming tokenUsage", {
|
|
54
|
+
incoming: updates.tokenUsage,
|
|
55
|
+
existing: existingTokenUsage,
|
|
56
|
+
});
|
|
50
57
|
// LangChain sends multiple token updates:
|
|
51
58
|
// 1. Early chunk: inputTokens (context) + outputTokens (estimate) + totalTokens
|
|
52
59
|
// 2. Later chunk: inputTokens=0 + outputTokens (final) + totalTokens (just output)
|
|
@@ -58,7 +65,9 @@ export const useChatStore = create((set) => ({
|
|
|
58
65
|
totalTokens: Math.max(updates.tokenUsage.inputTokens ?? 0, existingTokenUsage?.inputTokens ?? 0) +
|
|
59
66
|
Math.max(updates.tokenUsage.outputTokens ?? 0, existingTokenUsage?.outputTokens ?? 0),
|
|
60
67
|
};
|
|
61
|
-
|
|
68
|
+
logger.debug("updateMessage: merged tokenUsage", {
|
|
69
|
+
merged: messageMaxTokens,
|
|
70
|
+
});
|
|
62
71
|
// Replace the tokenUsage in updates with the max values
|
|
63
72
|
finalUpdates = {
|
|
64
73
|
...updates,
|
|
@@ -112,7 +121,7 @@ export const useChatStore = create((set) => ({
|
|
|
112
121
|
const isBilledCorrect = actualSum.inputTokens === newTotalBilled.inputTokens &&
|
|
113
122
|
actualSum.outputTokens === newTotalBilled.outputTokens &&
|
|
114
123
|
actualSum.totalTokens === newTotalBilled.totalTokens;
|
|
115
|
-
|
|
124
|
+
logger.debug("updateMessage: tokenUsage update", {
|
|
116
125
|
messageId: id,
|
|
117
126
|
updates: updates.tokenUsage,
|
|
118
127
|
existing: existingTokenUsage,
|
|
@@ -125,7 +134,7 @@ export const useChatStore = create((set) => ({
|
|
|
125
134
|
messageCount: messages.length,
|
|
126
135
|
messagesWithTokens: messageTokenBreakdown.length,
|
|
127
136
|
breakdown: messageTokenBreakdown,
|
|
128
|
-
})
|
|
137
|
+
});
|
|
129
138
|
}
|
|
130
139
|
return {
|
|
131
140
|
messages,
|
|
@@ -146,7 +155,7 @@ export const useChatStore = create((set) => ({
|
|
|
146
155
|
// Find the most recent assistant message (which should be streaming)
|
|
147
156
|
const lastAssistantIndex = state.messages.findLastIndex((msg) => msg.role === "assistant");
|
|
148
157
|
if (lastAssistantIndex === -1) {
|
|
149
|
-
|
|
158
|
+
logger.warn("No assistant message found to add tool call to");
|
|
150
159
|
return state;
|
|
151
160
|
}
|
|
152
161
|
const messages = [...state.messages];
|
|
@@ -163,7 +172,7 @@ export const useChatStore = create((set) => ({
|
|
|
163
172
|
// Find the most recent assistant message
|
|
164
173
|
const lastAssistantIndex = state.messages.findLastIndex((msg) => msg.role === "assistant");
|
|
165
174
|
if (lastAssistantIndex === -1) {
|
|
166
|
-
|
|
175
|
+
logger.warn("No assistant message found to update tool call in");
|
|
167
176
|
return state;
|
|
168
177
|
}
|
|
169
178
|
const messages = [...state.messages];
|
|
@@ -173,7 +182,7 @@ export const useChatStore = create((set) => ({
|
|
|
173
182
|
const toolCalls = lastAssistantMsg.toolCalls || [];
|
|
174
183
|
const existingIndex = toolCalls.findIndex((tc) => tc.id === update.id);
|
|
175
184
|
if (existingIndex === -1) {
|
|
176
|
-
|
|
185
|
+
logger.warn(`Tool call ${update.id} not found in message`);
|
|
177
186
|
return state;
|
|
178
187
|
}
|
|
179
188
|
const existing = toolCalls[existingIndex];
|
|
@@ -271,4 +280,9 @@ export const useChatStore = create((set) => ({
|
|
|
271
280
|
return state; // Should never happen, but satisfies TypeScript
|
|
272
281
|
return { tokenDisplayMode: nextMode };
|
|
273
282
|
}),
|
|
283
|
+
addLog: (log) => set((state) => ({
|
|
284
|
+
logs: [...state.logs, log],
|
|
285
|
+
})),
|
|
286
|
+
clearLogs: () => set({ logs: [] }),
|
|
287
|
+
setActiveTab: (tab) => set({ activeTab: tab }),
|
|
274
288
|
}));
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { createLogger } from "../../core/lib/logger.js";
|
|
1
2
|
import { HttpTransport } from "../transports/http.js";
|
|
2
3
|
import { StdioTransport } from "../transports/stdio.js";
|
|
3
4
|
import { WebSocketTransport } from "../transports/websocket.js";
|
|
5
|
+
const logger = createLogger("acp-client", "debug");
|
|
4
6
|
/**
|
|
5
7
|
* Simplified ACP client with explicit transport selection
|
|
6
8
|
*/
|
|
@@ -17,7 +19,9 @@ export class AcpClient {
|
|
|
17
19
|
this.setupTransportListeners();
|
|
18
20
|
if (config.autoConnect) {
|
|
19
21
|
this.connect().catch((error) => {
|
|
20
|
-
|
|
22
|
+
logger.error("Failed to auto-connect", {
|
|
23
|
+
error: error instanceof Error ? error.message : String(error),
|
|
24
|
+
});
|
|
21
25
|
});
|
|
22
26
|
}
|
|
23
27
|
}
|
|
@@ -191,7 +195,9 @@ export class AcpClient {
|
|
|
191
195
|
handler(update);
|
|
192
196
|
}
|
|
193
197
|
catch (error) {
|
|
194
|
-
|
|
198
|
+
logger.error("Error in session update handler", {
|
|
199
|
+
error: error instanceof Error ? error.message : String(error),
|
|
200
|
+
});
|
|
195
201
|
}
|
|
196
202
|
}
|
|
197
203
|
}
|
|
@@ -202,7 +208,9 @@ export class AcpClient {
|
|
|
202
208
|
handler(error);
|
|
203
209
|
}
|
|
204
210
|
catch (err) {
|
|
205
|
-
|
|
211
|
+
logger.error("Error in error handler", {
|
|
212
|
+
error: err instanceof Error ? err.message : String(err),
|
|
213
|
+
});
|
|
206
214
|
}
|
|
207
215
|
}
|
|
208
216
|
}
|
|
@@ -3,3 +3,6 @@ export interface ChatViewProps {
|
|
|
3
3
|
client: AcpClient | null;
|
|
4
4
|
}
|
|
5
5
|
export declare function ChatView({ client }: ChatViewProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
export declare function ChatViewStatus({ client }: {
|
|
7
|
+
client: AcpClient | null;
|
|
8
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -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 })
|
|
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,30 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text, useStdout } from "ink";
|
|
3
|
-
import {
|
|
2
|
+
import { Box, Text, useInput, useStdout } from "ink";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { SimpleTextInput } from "./SimpleTextInput.js";
|
|
4
5
|
export function InputBox({ value, isSubmitting, attachedFiles, onChange, onSubmit, onEscape, }) {
|
|
5
6
|
const { stdout } = useStdout();
|
|
6
7
|
const terminalWidth = stdout?.columns || 80;
|
|
7
|
-
|
|
8
|
+
// Handle special keys for multi-line and escape
|
|
9
|
+
useInput((_input, key) => {
|
|
10
|
+
// Escape key
|
|
11
|
+
if (key.escape && onEscape) {
|
|
12
|
+
onEscape();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
// Don't interfere if submitting
|
|
16
|
+
if (isSubmitting)
|
|
17
|
+
return;
|
|
18
|
+
// Shift+Enter or Alt+Enter: insert newline
|
|
19
|
+
if (key.return && (key.shift || key.meta)) {
|
|
20
|
+
onChange(value + "\n");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
// Split value into lines for display
|
|
25
|
+
const lines = value.split("\n");
|
|
26
|
+
const hasMultipleLines = lines.length > 1;
|
|
27
|
+
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
28
|
}
|
|
9
29
|
function formatFileSize(bytes) {
|
|
10
30
|
if (bytes < 1024)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { subscribeToLogs } from "../../core/lib/logger.js";
|
|
5
|
+
// Color mapping for log levels
|
|
6
|
+
const LOG_LEVEL_COLORS = {
|
|
7
|
+
trace: "gray",
|
|
8
|
+
debug: "blue",
|
|
9
|
+
info: "green",
|
|
10
|
+
warn: "yellow",
|
|
11
|
+
error: "red",
|
|
12
|
+
fatal: "red",
|
|
13
|
+
};
|
|
14
|
+
export function LogsPanel({ logs: initialLogs }) {
|
|
15
|
+
const [logs, setLogs] = useState(initialLogs);
|
|
16
|
+
// Subscribe to new logs
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const unsubscribe = subscribeToLogs((entry) => {
|
|
19
|
+
setLogs((prev) => [...prev, entry]);
|
|
20
|
+
});
|
|
21
|
+
return unsubscribe;
|
|
22
|
+
}, []);
|
|
23
|
+
if (logs.length === 0) {
|
|
24
|
+
return (_jsx(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(Text, { dimColor: true, children: "No logs yet..." }) }));
|
|
25
|
+
}
|
|
26
|
+
// Show last 100 logs
|
|
27
|
+
const displayLogs = logs.slice(-100);
|
|
28
|
+
return (_jsx(Box, { flexDirection: "column", paddingX: 1, children: displayLogs.map((log) => (_jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Text, { dimColor: true, children: new Date(log.timestamp).toLocaleTimeString() }), _jsxs(Text, { color: LOG_LEVEL_COLORS[log.level], bold: true, children: ["[", log.level.toUpperCase(), "]"] }), _jsxs(Text, { dimColor: true, children: ["[", log.service, "]"] }), _jsx(Text, { children: log.message }), log.metadata && (_jsx(Text, { dimColor: true, children: JSON.stringify(log.metadata) }))] }, log.id))) }));
|
|
29
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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(
|
|
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(
|
|
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.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -61,11 +61,10 @@
|
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@tailwindcss/postcss": "^4.1.17",
|
|
64
|
-
"@townco/tsconfig": "0.1.
|
|
64
|
+
"@townco/tsconfig": "0.1.20",
|
|
65
65
|
"@types/node": "^24.10.0",
|
|
66
66
|
"@types/react": "^19.2.2",
|
|
67
67
|
"ink": "^6.4.0",
|
|
68
|
-
"ink-text-input": "^6.0.0",
|
|
69
68
|
"react": "^19.2.0",
|
|
70
69
|
"tailwindcss": "^4.1.17",
|
|
71
70
|
"typescript": "^5.9.3"
|