@townco/ui 0.1.37 → 0.1.38
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-messages.d.ts +2 -0
- package/dist/core/hooks/use-chat-messages.js +19 -1
- package/dist/core/hooks/use-tool-calls.d.ts +2 -0
- package/dist/core/lib/logger.d.ts +0 -35
- package/dist/core/lib/logger.js +25 -108
- package/dist/core/schemas/chat.d.ts +4 -0
- package/dist/core/schemas/tool-call.d.ts +2 -0
- package/dist/core/schemas/tool-call.js +4 -0
- package/dist/gui/components/ChatEmptyState.d.ts +6 -0
- package/dist/gui/components/ChatEmptyState.js +2 -2
- package/dist/gui/components/ChatInput.js +1 -1
- package/dist/gui/components/ChatLayout.js +102 -8
- package/dist/gui/components/ChatPanelTabContent.d.ts +1 -1
- package/dist/gui/components/ChatPanelTabContent.js +10 -3
- package/dist/gui/components/ChatView.js +37 -13
- package/dist/gui/components/ContextUsageButton.d.ts +7 -0
- package/dist/gui/components/ContextUsageButton.js +18 -0
- package/dist/gui/components/FileSystemItem.js +6 -7
- package/dist/gui/components/FileSystemView.js +3 -3
- package/dist/gui/components/InlineToolCallSummary.d.ts +14 -0
- package/dist/gui/components/InlineToolCallSummary.js +110 -0
- package/dist/gui/components/InlineToolCallSummaryACP.d.ts +15 -0
- package/dist/gui/components/InlineToolCallSummaryACP.js +90 -0
- package/dist/gui/components/Response.js +2 -2
- package/dist/gui/components/SourceListItem.js +9 -1
- package/dist/gui/components/TodoListItem.js +19 -2
- package/dist/gui/components/ToolCall.js +21 -2
- package/dist/gui/components/Tooltip.d.ts +7 -0
- package/dist/gui/components/Tooltip.js +10 -0
- package/dist/gui/components/index.d.ts +4 -0
- package/dist/gui/components/index.js +5 -0
- package/dist/gui/components/tool-call-summary.d.ts +44 -0
- package/dist/gui/components/tool-call-summary.js +67 -0
- package/dist/gui/data/mockSourceData.d.ts +10 -0
- package/dist/gui/data/mockSourceData.js +40 -0
- package/dist/gui/data/mockTodoData.d.ts +10 -0
- package/dist/gui/data/mockTodoData.js +35 -0
- package/dist/gui/examples/FileSystemDemo.d.ts +5 -0
- package/dist/gui/examples/FileSystemDemo.js +24 -0
- package/dist/gui/examples/FileSystemExample.d.ts +17 -0
- package/dist/gui/examples/FileSystemExample.js +94 -0
- package/dist/sdk/schemas/session.d.ts +2 -0
- package/dist/sdk/transports/http.js +12 -0
- package/dist/sdk/transports/stdio.js +13 -0
- package/package.json +3 -3
- package/dist/tui/components/LogsPanel.d.ts +0 -5
- package/dist/tui/components/LogsPanel.js +0 -29
|
@@ -16,6 +16,8 @@ export declare function useChatMessages(client: AcpClient | null, startSession:
|
|
|
16
16
|
title: string;
|
|
17
17
|
kind: "read" | "edit" | "delete" | "move" | "search" | "execute" | "think" | "fetch" | "switch_mode" | "other";
|
|
18
18
|
status: "pending" | "in_progress" | "completed" | "failed";
|
|
19
|
+
prettyName?: string | undefined;
|
|
20
|
+
icon?: string | undefined;
|
|
19
21
|
contentPosition?: number | undefined;
|
|
20
22
|
locations?: {
|
|
21
23
|
path: string;
|
|
@@ -38,6 +38,8 @@ export function useChatMessages(client, startSession) {
|
|
|
38
38
|
sessionId: newSessionId,
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
|
+
// Create assistant message ID outside try block so it's accessible in catch
|
|
42
|
+
const assistantMessageId = `msg_${Date.now()}_assistant`;
|
|
41
43
|
try {
|
|
42
44
|
// Start streaming and track time immediately
|
|
43
45
|
const startTime = Date.now();
|
|
@@ -53,7 +55,6 @@ export function useChatMessages(client, startSession) {
|
|
|
53
55
|
};
|
|
54
56
|
addMessage(userMessage);
|
|
55
57
|
// Create placeholder for assistant message BEFORE sending
|
|
56
|
-
const assistantMessageId = `msg_${Date.now()}_assistant`;
|
|
57
58
|
const assistantMessage = {
|
|
58
59
|
id: assistantMessageId,
|
|
59
60
|
role: "assistant",
|
|
@@ -75,6 +76,7 @@ export function useChatMessages(client, startSession) {
|
|
|
75
76
|
});
|
|
76
77
|
// Listen for streaming chunks
|
|
77
78
|
let accumulatedContent = "";
|
|
79
|
+
let streamCompleted = false;
|
|
78
80
|
for await (const chunk of messageStream) {
|
|
79
81
|
if (chunk.tokenUsage) {
|
|
80
82
|
logger.debug("chunk.tokenUsage", {
|
|
@@ -91,6 +93,7 @@ export function useChatMessages(client, startSession) {
|
|
|
91
93
|
});
|
|
92
94
|
setIsStreaming(false);
|
|
93
95
|
setStreamingStartTime(null); // Clear global streaming start time
|
|
96
|
+
streamCompleted = true;
|
|
94
97
|
break;
|
|
95
98
|
}
|
|
96
99
|
else {
|
|
@@ -106,12 +109,27 @@ export function useChatMessages(client, startSession) {
|
|
|
106
109
|
}
|
|
107
110
|
}
|
|
108
111
|
}
|
|
112
|
+
// Ensure streaming state is cleared even if no explicit isComplete was received
|
|
113
|
+
if (!streamCompleted) {
|
|
114
|
+
logger.warn("Stream ended without isComplete flag");
|
|
115
|
+
updateMessage(assistantMessageId, {
|
|
116
|
+
isStreaming: false,
|
|
117
|
+
streamingStartTime: undefined,
|
|
118
|
+
});
|
|
119
|
+
setIsStreaming(false);
|
|
120
|
+
setStreamingStartTime(null);
|
|
121
|
+
}
|
|
109
122
|
}
|
|
110
123
|
catch (error) {
|
|
111
124
|
const message = error instanceof Error ? error.message : String(error);
|
|
112
125
|
setError(message);
|
|
113
126
|
setIsStreaming(false);
|
|
114
127
|
setStreamingStartTime(null); // Clear streaming start time on error
|
|
128
|
+
// Ensure the assistant message isStreaming is set to false
|
|
129
|
+
updateMessage(assistantMessageId, {
|
|
130
|
+
isStreaming: false,
|
|
131
|
+
streamingStartTime: undefined,
|
|
132
|
+
});
|
|
115
133
|
}
|
|
116
134
|
}, [
|
|
117
135
|
client,
|
|
@@ -14,6 +14,8 @@ export declare function useToolCalls(client: AcpClient | null): {
|
|
|
14
14
|
title: string;
|
|
15
15
|
kind: "read" | "edit" | "delete" | "move" | "search" | "execute" | "think" | "fetch" | "switch_mode" | "other";
|
|
16
16
|
status: "pending" | "in_progress" | "completed" | "failed";
|
|
17
|
+
prettyName?: string | undefined;
|
|
18
|
+
icon?: string | undefined;
|
|
17
19
|
contentPosition?: number | undefined;
|
|
18
20
|
locations?: {
|
|
19
21
|
path: string;
|
|
@@ -1,46 +1,12 @@
|
|
|
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
|
|
6
4
|
*/
|
|
7
5
|
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;
|
|
34
6
|
export declare class Logger {
|
|
35
7
|
private service;
|
|
36
8
|
private minLevel;
|
|
37
|
-
private logFilePath?;
|
|
38
|
-
private logsDir?;
|
|
39
|
-
private writeQueue;
|
|
40
|
-
private isWriting;
|
|
41
9
|
constructor(service: string, minLevel?: LogLevel);
|
|
42
|
-
private setupFileLogging;
|
|
43
|
-
private writeToFile;
|
|
44
10
|
private shouldLog;
|
|
45
11
|
private log;
|
|
46
12
|
trace(message: string, metadata?: Record<string, unknown>): void;
|
|
@@ -56,4 +22,3 @@ export declare class Logger {
|
|
|
56
22
|
* @param minLevel - Minimum log level to display (default: "debug")
|
|
57
23
|
*/
|
|
58
24
|
export declare function createLogger(service: string, minLevel?: LogLevel): Logger;
|
|
59
|
-
export {};
|
package/dist/core/lib/logger.js
CHANGED
|
@@ -1,45 +1,7 @@
|
|
|
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
|
|
6
4
|
*/
|
|
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
|
-
}
|
|
43
5
|
const LOG_LEVELS = {
|
|
44
6
|
trace: 0,
|
|
45
7
|
debug: 1,
|
|
@@ -56,7 +18,7 @@ const _LOG_COLORS = {
|
|
|
56
18
|
error: "#EF4444", // red
|
|
57
19
|
fatal: "#DC2626", // dark red
|
|
58
20
|
};
|
|
59
|
-
const
|
|
21
|
+
const LOG_STYLES = {
|
|
60
22
|
trace: "color: #6B7280",
|
|
61
23
|
debug: "color: #3B82F6; font-weight: bold",
|
|
62
24
|
info: "color: #10B981; font-weight: bold",
|
|
@@ -67,10 +29,6 @@ const _LOG_STYLES = {
|
|
|
67
29
|
export class Logger {
|
|
68
30
|
service;
|
|
69
31
|
minLevel;
|
|
70
|
-
logFilePath;
|
|
71
|
-
logsDir;
|
|
72
|
-
writeQueue = [];
|
|
73
|
-
isWriting = false;
|
|
74
32
|
constructor(service, minLevel = "debug") {
|
|
75
33
|
this.service = service;
|
|
76
34
|
this.minLevel = minLevel;
|
|
@@ -79,48 +37,6 @@ export class Logger {
|
|
|
79
37
|
process.env?.NODE_ENV === "production") {
|
|
80
38
|
this.minLevel = "info";
|
|
81
39
|
}
|
|
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;
|
|
124
40
|
}
|
|
125
41
|
shouldLog(level) {
|
|
126
42
|
return LOG_LEVELS[level] >= LOG_LEVELS[this.minLevel];
|
|
@@ -130,37 +46,38 @@ export class Logger {
|
|
|
130
46
|
return;
|
|
131
47
|
}
|
|
132
48
|
const entry = {
|
|
133
|
-
id: `log_${++logIdCounter}`,
|
|
134
49
|
timestamp: new Date().toISOString(),
|
|
135
50
|
level,
|
|
136
51
|
service: this.service,
|
|
137
52
|
message,
|
|
138
53
|
...(metadata && { metadata }),
|
|
139
54
|
};
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
55
|
+
// Console output with color-coding
|
|
56
|
+
const style = LOG_STYLES[level];
|
|
57
|
+
const levelUpper = level.toUpperCase().padEnd(5);
|
|
58
|
+
if (typeof console !== "undefined") {
|
|
59
|
+
// Format: [timestamp] [SERVICE] [LEVEL] message
|
|
60
|
+
const prefix = `%c[${entry.timestamp}] [${this.service}] [${levelUpper}]`;
|
|
61
|
+
const msg = metadata ? `${message} %o` : message;
|
|
62
|
+
switch (level) {
|
|
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
|
+
}
|
|
148
76
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
});
|
|
77
|
+
// Also log the structured JSON for debugging
|
|
78
|
+
if (level === "fatal" || level === "error") {
|
|
79
|
+
console.debug("Structured log:", JSON.stringify(entry));
|
|
161
80
|
}
|
|
162
|
-
// No console output - logs are only captured and displayed in UI
|
|
163
|
-
// This prevents logs from polluting stdout/stderr in TUI mode
|
|
164
81
|
}
|
|
165
82
|
trace(message, metadata) {
|
|
166
83
|
this.log("trace", message, metadata);
|
|
@@ -20,6 +20,8 @@ export declare const DisplayMessage: z.ZodObject<{
|
|
|
20
20
|
toolCalls: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
21
21
|
id: z.ZodString;
|
|
22
22
|
title: z.ZodString;
|
|
23
|
+
prettyName: z.ZodOptional<z.ZodString>;
|
|
24
|
+
icon: z.ZodOptional<z.ZodString>;
|
|
23
25
|
kind: z.ZodEnum<{
|
|
24
26
|
read: "read";
|
|
25
27
|
edit: "edit";
|
|
@@ -116,6 +118,8 @@ export declare const ChatSessionState: z.ZodObject<{
|
|
|
116
118
|
toolCalls: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
117
119
|
id: z.ZodString;
|
|
118
120
|
title: z.ZodString;
|
|
121
|
+
prettyName: z.ZodOptional<z.ZodString>;
|
|
122
|
+
icon: z.ZodOptional<z.ZodString>;
|
|
119
123
|
kind: z.ZodEnum<{
|
|
120
124
|
read: "read";
|
|
121
125
|
edit: "edit";
|
|
@@ -71,6 +71,8 @@ export type ToolCallContentBlock = z.infer<typeof ToolCallContentBlockSchema>;
|
|
|
71
71
|
export declare const ToolCallSchema: z.ZodObject<{
|
|
72
72
|
id: z.ZodString;
|
|
73
73
|
title: z.ZodString;
|
|
74
|
+
prettyName: z.ZodOptional<z.ZodString>;
|
|
75
|
+
icon: z.ZodOptional<z.ZodString>;
|
|
74
76
|
kind: z.ZodEnum<{
|
|
75
77
|
read: "read";
|
|
76
78
|
edit: "edit";
|
|
@@ -75,6 +75,10 @@ export const ToolCallSchema = z.object({
|
|
|
75
75
|
id: z.string(),
|
|
76
76
|
/** Human-readable description of the operation */
|
|
77
77
|
title: z.string(),
|
|
78
|
+
/** Optional pretty name for the tool (e.g. "Web Search" instead of "web_search") */
|
|
79
|
+
prettyName: z.string().optional(),
|
|
80
|
+
/** Optional icon identifier for the tool (e.g. "Globe", "Search", "Edit") */
|
|
81
|
+
icon: z.string().optional(),
|
|
78
82
|
/** Category for UI presentation */
|
|
79
83
|
kind: ToolCallKindSchema,
|
|
80
84
|
/** Current execution status */
|
|
@@ -14,5 +14,11 @@ export interface ChatEmptyStateProps extends React.HTMLAttributes<HTMLDivElement
|
|
|
14
14
|
onPromptClick?: (prompt: string) => void;
|
|
15
15
|
/** Callback when guide is clicked */
|
|
16
16
|
onGuideClick?: () => void;
|
|
17
|
+
/** Callback when "Open Files" is clicked */
|
|
18
|
+
onOpenFiles?: () => void;
|
|
19
|
+
/** Callback when hovering over a prompt */
|
|
20
|
+
onPromptHover?: (prompt: string) => void;
|
|
21
|
+
/** Callback when mouse leaves a prompt */
|
|
22
|
+
onPromptLeave?: () => void;
|
|
17
23
|
}
|
|
18
24
|
export declare const ChatEmptyState: React.ForwardRefExoticComponent<ChatEmptyStateProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { ChevronRight } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { cn } from "../lib/utils.js";
|
|
5
|
-
export const ChatEmptyState = React.forwardRef(({ title, description, guideUrl, guideText = "Guide", suggestedPrompts = [], onPromptClick, onGuideClick, className, ...props }, ref) => {
|
|
5
|
+
export const ChatEmptyState = React.forwardRef(({ title, description, guideUrl, guideText = "Guide", suggestedPrompts = [], onPromptClick, onGuideClick, onOpenFiles, onPromptHover, onPromptLeave, className, ...props }, ref) => {
|
|
6
6
|
const handlePromptClick = (prompt) => {
|
|
7
7
|
onPromptClick?.(prompt);
|
|
8
8
|
};
|
|
@@ -17,6 +17,6 @@ export const ChatEmptyState = React.forwardRef(({ title, description, guideUrl,
|
|
|
17
17
|
for (let i = 0; i < suggestedPrompts.length; i += 2) {
|
|
18
18
|
promptRows.push(suggestedPrompts.slice(i, i + 2));
|
|
19
19
|
}
|
|
20
|
-
return (_jsxs("div", { ref: ref, className: cn("flex flex-col items-start
|
|
20
|
+
return (_jsxs("div", { ref: ref, className: cn("flex flex-col items-start", className), ...props, children: [_jsx("h3", { className: "text-heading-4 text-text-primary mb-6", children: title }), _jsx("p", { className: "text-subheading text-text-secondary max-w-prose mb-6", children: description }), onOpenFiles && (_jsxs("button", { type: "button", onClick: onOpenFiles, className: "inline-flex items-center gap-1 py-1.5 pr-3 -ml-3 pl-3 rounded-lg hover:bg-accent transition-colors mb-6", children: [_jsx("span", { className: "text-paragraph-sm-medium text-foreground tracking-wide leading-none", children: "View Files" }), _jsx(ChevronRight, { className: "size-4 text-foreground shrink-0" })] })), (guideUrl || onGuideClick) && (_jsxs("button", { type: "button", onClick: handleGuideClick, className: "inline-flex items-center gap-1 py-1.5 pr-3 -ml-3 pl-3 rounded-lg hover:bg-accent transition-colors", children: [_jsx("span", { className: "text-paragraph-sm-medium text-foreground tracking-wide leading-none", children: guideText }), _jsx(ChevronRight, { className: "size-4 text-foreground shrink-0" })] })), suggestedPrompts.length > 0 && (_jsxs("div", { className: "flex flex-col gap-3 w-full max-w-prompt-container", children: [_jsx("p", { className: "text-label text-text-tertiary", children: "Suggested Prompts" }), _jsx("div", { className: "flex flex-col gap-2.5", children: promptRows.map((row) => (_jsx("div", { className: "flex gap-2.5 items-center", children: row.map((prompt) => (_jsx("button", { type: "button", onClick: () => handlePromptClick(prompt), onMouseEnter: () => onPromptHover?.(prompt), onMouseLeave: () => onPromptLeave?.(), className: "flex-1 flex items-start gap-2 p-3 bg-secondary hover:bg-secondary/80 rounded-2xl transition-colors min-w-0", children: _jsx("span", { className: "text-paragraph font-normal leading-normal text-text-tertiary truncate", children: prompt }) }, prompt))) }, row.join("-")))) })] }))] }));
|
|
21
21
|
});
|
|
22
22
|
ChatEmptyState.displayName = "ChatEmptyState";
|
|
@@ -206,7 +206,7 @@ const ChatInputField = React.forwardRef(({ asChild = false, className, onKeyDown
|
|
|
206
206
|
if (asChild && React.isValidElement(children)) {
|
|
207
207
|
return React.cloneElement(children, fieldProps);
|
|
208
208
|
}
|
|
209
|
-
return (_jsx("textarea", { ...fieldProps, className: cn("w-full resize-none rounded-none border-none p-4 shadow-none", "outline-none ring-0
|
|
209
|
+
return (_jsx("textarea", { ...fieldProps, rows: 1, className: cn("w-full resize-none rounded-none border-none p-4 shadow-none", "outline-none ring-0 max-h-[6lh] min-h-[44px]", "bg-transparent dark:bg-transparent focus-visible:ring-0", "text-paragraph-sm placeholder:text-muted-foreground", "disabled:cursor-not-allowed disabled:opacity-50", className) }));
|
|
210
210
|
});
|
|
211
211
|
ChatInputField.displayName = "ChatInput.Field";
|
|
212
212
|
const ChatInputSubmit = React.forwardRef(({ asChild = false, className, disabled: disabledProp, children, ...props }, ref) => {
|
|
@@ -41,38 +41,118 @@ ChatLayoutBody.displayName = "ChatLayout.Body";
|
|
|
41
41
|
const ChatLayoutMessages = React.forwardRef(({ className, children, onScrollChange, showScrollToBottom = true, ...props }, ref) => {
|
|
42
42
|
const [showScrollButton, setShowScrollButton] = React.useState(false);
|
|
43
43
|
const scrollContainerRef = React.useRef(null);
|
|
44
|
+
const wasAtBottomRef = React.useRef(true); // Track if user was at bottom before content update
|
|
45
|
+
const isAutoScrollingRef = React.useRef(false); // Track if we're programmatically scrolling
|
|
44
46
|
// Merge refs
|
|
45
47
|
React.useImperativeHandle(ref, () => scrollContainerRef.current);
|
|
46
48
|
// Check if user is at bottom of scroll
|
|
47
49
|
const checkScrollPosition = React.useCallback(() => {
|
|
48
50
|
const container = scrollContainerRef.current;
|
|
49
51
|
if (!container)
|
|
50
|
-
return;
|
|
52
|
+
return false;
|
|
51
53
|
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
52
54
|
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
|
53
55
|
const isAtBottom = distanceFromBottom < 100; // 100px threshold
|
|
54
56
|
setShowScrollButton(!isAtBottom && showScrollToBottom);
|
|
55
57
|
onScrollChange?.(isAtBottom);
|
|
58
|
+
return isAtBottom;
|
|
56
59
|
}, [onScrollChange, showScrollToBottom]);
|
|
57
60
|
// Handle scroll events
|
|
58
61
|
const handleScroll = React.useCallback(() => {
|
|
59
|
-
|
|
62
|
+
// If this is a programmatic scroll, don't update wasAtBottomRef
|
|
63
|
+
if (isAutoScrollingRef.current) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// This is a user-initiated scroll, update the position
|
|
67
|
+
const isAtBottom = checkScrollPosition();
|
|
68
|
+
wasAtBottomRef.current = isAtBottom;
|
|
60
69
|
}, [checkScrollPosition]);
|
|
61
70
|
// Scroll to bottom function
|
|
62
|
-
const scrollToBottom = React.useCallback(() => {
|
|
71
|
+
const scrollToBottom = React.useCallback((smooth = true) => {
|
|
63
72
|
const container = scrollContainerRef.current;
|
|
64
73
|
if (!container)
|
|
65
74
|
return;
|
|
75
|
+
// Mark that we're about to programmatically scroll
|
|
76
|
+
isAutoScrollingRef.current = true;
|
|
77
|
+
wasAtBottomRef.current = true; // Set immediately for instant scrolls
|
|
66
78
|
container.scrollTo({
|
|
67
79
|
top: container.scrollHeight,
|
|
68
|
-
behavior: "smooth",
|
|
80
|
+
behavior: smooth ? "smooth" : "auto",
|
|
69
81
|
});
|
|
82
|
+
// Clear the flag after scroll completes
|
|
83
|
+
// For instant scrolling, clear immediately; for smooth, wait
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
isAutoScrollingRef.current = false;
|
|
86
|
+
}, smooth ? 300 : 0);
|
|
70
87
|
}, []);
|
|
71
|
-
//
|
|
88
|
+
// Auto-scroll when content changes if user was at bottom
|
|
89
|
+
React.useEffect(() => {
|
|
90
|
+
const container = scrollContainerRef.current;
|
|
91
|
+
if (!container)
|
|
92
|
+
return;
|
|
93
|
+
// If user was at the bottom, scroll to new content
|
|
94
|
+
if (wasAtBottomRef.current && !isAutoScrollingRef.current) {
|
|
95
|
+
// Use requestAnimationFrame to ensure DOM has updated
|
|
96
|
+
requestAnimationFrame(() => {
|
|
97
|
+
scrollToBottom(false); // Use instant scroll for streaming to avoid jarring smooth animations
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
// Update scroll position state (but don't change wasAtBottomRef if we're auto-scrolling)
|
|
101
|
+
if (!isAutoScrollingRef.current) {
|
|
102
|
+
checkScrollPosition();
|
|
103
|
+
}
|
|
104
|
+
}, [children, scrollToBottom, checkScrollPosition]);
|
|
105
|
+
// Check scroll position on mount
|
|
72
106
|
React.useEffect(() => {
|
|
73
|
-
|
|
107
|
+
if (!isAutoScrollingRef.current) {
|
|
108
|
+
const isAtBottom = checkScrollPosition();
|
|
109
|
+
wasAtBottomRef.current = isAtBottom;
|
|
110
|
+
}
|
|
74
111
|
}, [checkScrollPosition]);
|
|
75
|
-
|
|
112
|
+
// Detect user interaction with scroll area (wheel, touch) - IMMEDIATELY break auto-scroll
|
|
113
|
+
const handleUserInteraction = React.useCallback(() => {
|
|
114
|
+
// Immediately mark that user is interacting
|
|
115
|
+
isAutoScrollingRef.current = false;
|
|
116
|
+
// For wheel/touch events, temporarily break auto-scroll
|
|
117
|
+
// The actual scroll event will update wasAtBottomRef properly
|
|
118
|
+
// This prevents the race condition where content updates before scroll completes
|
|
119
|
+
const container = scrollContainerRef.current;
|
|
120
|
+
if (!container)
|
|
121
|
+
return;
|
|
122
|
+
// Check current position BEFORE the scroll happens
|
|
123
|
+
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
124
|
+
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
|
125
|
+
// If user is not currently at the bottom, definitely break auto-scroll
|
|
126
|
+
if (distanceFromBottom >= 100) {
|
|
127
|
+
wasAtBottomRef.current = false;
|
|
128
|
+
}
|
|
129
|
+
// If they are at bottom, the scroll event will determine if they stay there
|
|
130
|
+
}, []);
|
|
131
|
+
// Handle keyboard navigation
|
|
132
|
+
const handleKeyDown = React.useCallback((e) => {
|
|
133
|
+
// If user presses arrow keys, page up/down, home/end - they're scrolling
|
|
134
|
+
const scrollKeys = [
|
|
135
|
+
"ArrowUp",
|
|
136
|
+
"ArrowDown",
|
|
137
|
+
"PageUp",
|
|
138
|
+
"PageDown",
|
|
139
|
+
"Home",
|
|
140
|
+
"End",
|
|
141
|
+
];
|
|
142
|
+
if (scrollKeys.includes(e.key)) {
|
|
143
|
+
isAutoScrollingRef.current = false;
|
|
144
|
+
// Check position on next frame after the scroll happens
|
|
145
|
+
requestAnimationFrame(() => {
|
|
146
|
+
const container = scrollContainerRef.current;
|
|
147
|
+
if (!container)
|
|
148
|
+
return;
|
|
149
|
+
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
150
|
+
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
|
151
|
+
wasAtBottomRef.current = distanceFromBottom < 100;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}, []);
|
|
155
|
+
return (_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [_jsx("div", { ref: scrollContainerRef, className: cn("h-full overflow-y-auto flex flex-col", className), onScroll: handleScroll, onWheel: handleUserInteraction, onTouchStart: handleUserInteraction, onKeyDown: handleKeyDown, tabIndex: 0, ...props, children: _jsx("div", { className: "mx-auto max-w-chat flex-1 w-full flex flex-col", children: children }) }), showScrollButton && (_jsx("button", { type: "button", onClick: () => scrollToBottom(true), className: cn("absolute bottom-4 left-1/2 -translate-x-1/2 z-10", "flex items-center justify-center p-2 rounded-full", "bg-card border border-border shadow-lg", "text-foreground", "hover:bg-accent hover:text-accent-foreground", "transition-all duration-200 ease-in-out", "animate-in fade-in slide-in-from-bottom-2"), "aria-label": "Scroll to bottom", children: _jsx(ArrowDown, { className: "size-4" }) }))] }));
|
|
76
156
|
});
|
|
77
157
|
ChatLayoutMessages.displayName = "ChatLayout.Messages";
|
|
78
158
|
const ChatLayoutFooter = React.forwardRef(({ className, children, ...props }, ref) => {
|
|
@@ -88,10 +168,24 @@ const ChatLayoutSidebar = React.forwardRef(({ className, children, ...props }, r
|
|
|
88
168
|
ChatLayoutSidebar.displayName = "ChatLayout.Sidebar";
|
|
89
169
|
const ChatLayoutAside = React.forwardRef(({ breakpoint = "lg", className, children, ...props }, ref) => {
|
|
90
170
|
const { panelSize } = useChatLayoutContext();
|
|
171
|
+
const [minSizePercent, setMinSizePercent] = React.useState(25);
|
|
172
|
+
// Convert 400px minimum to percentage based on window width
|
|
173
|
+
React.useEffect(() => {
|
|
174
|
+
const updateMinSize = () => {
|
|
175
|
+
const minPixels = 400;
|
|
176
|
+
const minPercent = (minPixels / window.innerWidth) * 100;
|
|
177
|
+
setMinSizePercent(Math.max(minPercent, 25)); // Never less than 25% or 400px
|
|
178
|
+
};
|
|
179
|
+
updateMinSize();
|
|
180
|
+
window.addEventListener("resize", updateMinSize);
|
|
181
|
+
return () => {
|
|
182
|
+
window.removeEventListener("resize", updateMinSize);
|
|
183
|
+
};
|
|
184
|
+
}, []);
|
|
91
185
|
// Hidden state - don't render
|
|
92
186
|
if (panelSize === "hidden")
|
|
93
187
|
return null;
|
|
94
|
-
return (_jsxs(_Fragment, { children: [_jsx(ResizableHandle, { withHandle: true }), _jsx(ResizablePanel, { defaultSize: 25, minSize:
|
|
188
|
+
return (_jsxs(_Fragment, { children: [_jsx(ResizableHandle, { withHandle: true, className: "group-hover:opacity-100 opacity-0 transition-opacity" }), _jsx(ResizablePanel, { defaultSize: 25, minSize: minSizePercent, maxSize: 35, className: "group", children: _jsx("div", { ref: ref, className: cn(
|
|
95
189
|
// Hidden by default, visible at breakpoint
|
|
96
190
|
"hidden h-full border-l border-border bg-card overflow-y-auto transition-all duration-300",
|
|
97
191
|
// Breakpoint visibility
|
|
@@ -7,7 +7,7 @@ import type { TodoItem } from "./TodoListItem.js";
|
|
|
7
7
|
* Following component architecture best practices
|
|
8
8
|
*/
|
|
9
9
|
export interface TodoTabContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
10
|
-
todos
|
|
10
|
+
todos?: TodoItem[];
|
|
11
11
|
}
|
|
12
12
|
export declare const TodoTabContent: React.ForwardRefExoticComponent<TodoTabContentProps & React.RefAttributes<HTMLDivElement>>;
|
|
13
13
|
export interface FilesTabContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
|
+
import { mockSourceData } from "../data/mockSourceData.js";
|
|
4
|
+
import { mockTodoData } from "../data/mockTodoData.js";
|
|
3
5
|
import { cn } from "../lib/utils.js";
|
|
4
6
|
import { FileSystemView } from "./FileSystemView.js";
|
|
5
7
|
import { SourceListItem } from "./SourceListItem.js";
|
|
8
|
+
import { TodoList } from "./TodoList.js";
|
|
6
9
|
export const TodoTabContent = React.forwardRef(({ todos, className, ...props }, ref) => {
|
|
7
|
-
|
|
10
|
+
// Use mock data if no todos provided or if empty array
|
|
11
|
+
const displayTodos = todos && todos.length > 0 ? todos : mockTodoData;
|
|
12
|
+
return (_jsx("div", { ref: ref, className: cn("space-y-2", className), ...props, children: _jsx(TodoList, { todos: displayTodos }) }));
|
|
8
13
|
});
|
|
9
14
|
TodoTabContent.displayName = "TodoTabContent";
|
|
10
15
|
export const FilesTabContent = React.forwardRef(({ files = [], provider, onFileSelect, className, ...props }, ref) => {
|
|
@@ -29,8 +34,10 @@ export const FilesTabContent = React.forwardRef(({ files = [], provider, onFileS
|
|
|
29
34
|
}, onDownload: handleDownload, onRename: handleRename, onDelete: handleDelete, className: "h-full" }) }));
|
|
30
35
|
});
|
|
31
36
|
FilesTabContent.displayName = "FilesTabContent";
|
|
32
|
-
export const SourcesTabContent = React.forwardRef(({ sources
|
|
33
|
-
|
|
37
|
+
export const SourcesTabContent = React.forwardRef(({ sources, className, ...props }, ref) => {
|
|
38
|
+
// Use mock data if no sources provided or if empty array
|
|
39
|
+
const displaySources = sources && sources.length > 0 ? sources : mockSourceData;
|
|
40
|
+
return (_jsx("div", { ref: ref, className: cn("space-y-2", className), ...props, children: displaySources.map((source) => (_jsx(SourceListItem, { source: source }, source.id))) }));
|
|
34
41
|
});
|
|
35
42
|
SourcesTabContent.displayName = "SourcesTabContent";
|
|
36
43
|
export const DatabaseTabContent = React.forwardRef(({ data, className, ...props }, ref) => {
|