@townco/ui 0.1.36 → 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.
Files changed (53) hide show
  1. package/dist/core/hooks/use-chat-input.d.ts +1 -1
  2. package/dist/core/hooks/use-chat-input.js +2 -2
  3. package/dist/core/hooks/use-chat-messages.d.ts +3 -1
  4. package/dist/core/hooks/use-chat-messages.js +36 -7
  5. package/dist/core/hooks/use-chat-session.d.ts +1 -1
  6. package/dist/core/hooks/use-chat-session.js +4 -7
  7. package/dist/core/hooks/use-tool-calls.d.ts +2 -0
  8. package/dist/core/lib/logger.d.ts +24 -0
  9. package/dist/core/lib/logger.js +108 -0
  10. package/dist/core/schemas/chat.d.ts +4 -0
  11. package/dist/core/schemas/tool-call.d.ts +2 -0
  12. package/dist/core/schemas/tool-call.js +4 -0
  13. package/dist/gui/components/ChatEmptyState.d.ts +6 -0
  14. package/dist/gui/components/ChatEmptyState.js +2 -2
  15. package/dist/gui/components/ChatInput.d.ts +4 -0
  16. package/dist/gui/components/ChatInput.js +12 -7
  17. package/dist/gui/components/ChatLayout.js +102 -8
  18. package/dist/gui/components/ChatPanelTabContent.d.ts +1 -1
  19. package/dist/gui/components/ChatPanelTabContent.js +10 -3
  20. package/dist/gui/components/ChatView.js +45 -14
  21. package/dist/gui/components/ContextUsageButton.d.ts +7 -0
  22. package/dist/gui/components/ContextUsageButton.js +18 -0
  23. package/dist/gui/components/FileSystemItem.js +6 -7
  24. package/dist/gui/components/FileSystemView.js +3 -3
  25. package/dist/gui/components/InlineToolCallSummary.d.ts +14 -0
  26. package/dist/gui/components/InlineToolCallSummary.js +110 -0
  27. package/dist/gui/components/InlineToolCallSummaryACP.d.ts +15 -0
  28. package/dist/gui/components/InlineToolCallSummaryACP.js +90 -0
  29. package/dist/gui/components/MessageContent.js +1 -1
  30. package/dist/gui/components/Response.js +2 -2
  31. package/dist/gui/components/SourceListItem.js +9 -1
  32. package/dist/gui/components/TodoListItem.js +19 -2
  33. package/dist/gui/components/ToolCall.js +21 -2
  34. package/dist/gui/components/Tooltip.d.ts +7 -0
  35. package/dist/gui/components/Tooltip.js +10 -0
  36. package/dist/gui/components/index.d.ts +4 -0
  37. package/dist/gui/components/index.js +5 -0
  38. package/dist/gui/components/tool-call-summary.d.ts +44 -0
  39. package/dist/gui/components/tool-call-summary.js +67 -0
  40. package/dist/gui/data/mockSourceData.d.ts +10 -0
  41. package/dist/gui/data/mockSourceData.js +40 -0
  42. package/dist/gui/data/mockTodoData.d.ts +10 -0
  43. package/dist/gui/data/mockTodoData.js +35 -0
  44. package/dist/gui/examples/FileSystemDemo.d.ts +5 -0
  45. package/dist/gui/examples/FileSystemDemo.js +24 -0
  46. package/dist/gui/examples/FileSystemExample.d.ts +17 -0
  47. package/dist/gui/examples/FileSystemExample.js +94 -0
  48. package/dist/sdk/schemas/session.d.ts +2 -0
  49. package/dist/sdk/transports/http.d.ts +1 -0
  50. package/dist/sdk/transports/http.js +40 -8
  51. package/dist/sdk/transports/stdio.js +13 -0
  52. package/dist/tui/components/ChatView.js +5 -5
  53. package/package.json +3 -3
@@ -2,7 +2,7 @@ import type { AcpClient } from "../../sdk/client/index.js";
2
2
  /**
3
3
  * Hook for managing chat input
4
4
  */
5
- export declare function useChatInput(client: AcpClient | null): {
5
+ export declare function useChatInput(client: AcpClient | null, startSession: () => Promise<string | null>): {
6
6
  value: string;
7
7
  isSubmitting: boolean;
8
8
  attachedFiles: {
@@ -6,13 +6,13 @@ const logger = createLogger("use-chat-input", "debug");
6
6
  /**
7
7
  * Hook for managing chat input
8
8
  */
9
- export function useChatInput(client) {
9
+ export function useChatInput(client, startSession) {
10
10
  const input = useChatStore((state) => state.input);
11
11
  const setInputValue = useChatStore((state) => state.setInputValue);
12
12
  const setInputSubmitting = useChatStore((state) => state.setInputSubmitting);
13
13
  const addFileAttachment = useChatStore((state) => state.addFileAttachment);
14
14
  const removeFileAttachment = useChatStore((state) => state.removeFileAttachment);
15
- const { sendMessage } = useChatMessages(client);
15
+ const { sendMessage } = useChatMessages(client, startSession);
16
16
  /**
17
17
  * Handle input value change
18
18
  */
@@ -2,7 +2,7 @@ import type { AcpClient } from "../../sdk/client/index.js";
2
2
  /**
3
3
  * Hook for managing chat messages
4
4
  */
5
- export declare function useChatMessages(client: AcpClient | null): {
5
+ export declare function useChatMessages(client: AcpClient | null, startSession: () => Promise<string | null>): {
6
6
  messages: {
7
7
  id: string;
8
8
  role: "user" | "assistant" | "system";
@@ -16,6 +16,8 @@ export declare function useChatMessages(client: AcpClient | null): {
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;
@@ -5,7 +5,7 @@ const logger = createLogger("use-chat-messages", "debug");
5
5
  /**
6
6
  * Hook for managing chat messages
7
7
  */
8
- export function useChatMessages(client) {
8
+ export function useChatMessages(client, startSession) {
9
9
  const messages = useChatStore((state) => state.messages);
10
10
  const isStreaming = useChatStore((state) => state.isStreaming);
11
11
  const sessionId = useChatStore((state) => state.sessionId);
@@ -23,11 +23,23 @@ export function useChatMessages(client) {
23
23
  setError("No client available");
24
24
  return;
25
25
  }
26
- if (!sessionId) {
27
- logger.error("No active session");
28
- setError("No active session");
29
- return;
26
+ // Create session lazily if it doesn't exist
27
+ let activeSessionId = sessionId;
28
+ if (!activeSessionId) {
29
+ logger.info("Creating new session before sending first message");
30
+ const newSessionId = await startSession();
31
+ if (!newSessionId) {
32
+ logger.error("Failed to create session");
33
+ setError("Failed to create session");
34
+ return;
35
+ }
36
+ activeSessionId = newSessionId;
37
+ logger.info("Session created successfully", {
38
+ sessionId: newSessionId,
39
+ });
30
40
  }
41
+ // Create assistant message ID outside try block so it's accessible in catch
42
+ const assistantMessageId = `msg_${Date.now()}_assistant`;
31
43
  try {
32
44
  // Start streaming and track time immediately
33
45
  const startTime = Date.now();
@@ -43,7 +55,6 @@ export function useChatMessages(client) {
43
55
  };
44
56
  addMessage(userMessage);
45
57
  // Create placeholder for assistant message BEFORE sending
46
- const assistantMessageId = `msg_${Date.now()}_assistant`;
47
58
  const assistantMessage = {
48
59
  id: assistantMessageId,
49
60
  role: "assistant",
@@ -57,7 +68,7 @@ export function useChatMessages(client) {
57
68
  const messageStream = client.receiveMessages();
58
69
  // Send ONLY the new message (not full history)
59
70
  // The agent backend now manages conversation context
60
- client.sendMessage(content, sessionId).catch((error) => {
71
+ client.sendMessage(content, activeSessionId).catch((error) => {
61
72
  const message = error instanceof Error ? error.message : String(error);
62
73
  setError(message);
63
74
  setIsStreaming(false);
@@ -65,6 +76,7 @@ export function useChatMessages(client) {
65
76
  });
66
77
  // Listen for streaming chunks
67
78
  let accumulatedContent = "";
79
+ let streamCompleted = false;
68
80
  for await (const chunk of messageStream) {
69
81
  if (chunk.tokenUsage) {
70
82
  logger.debug("chunk.tokenUsage", {
@@ -81,6 +93,7 @@ export function useChatMessages(client) {
81
93
  });
82
94
  setIsStreaming(false);
83
95
  setStreamingStartTime(null); // Clear global streaming start time
96
+ streamCompleted = true;
84
97
  break;
85
98
  }
86
99
  else {
@@ -96,16 +109,32 @@ export function useChatMessages(client) {
96
109
  }
97
110
  }
98
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
+ }
99
122
  }
100
123
  catch (error) {
101
124
  const message = error instanceof Error ? error.message : String(error);
102
125
  setError(message);
103
126
  setIsStreaming(false);
104
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
+ });
105
133
  }
106
134
  }, [
107
135
  client,
108
136
  sessionId,
137
+ startSession,
109
138
  addMessage,
110
139
  updateMessage,
111
140
  setIsStreaming,
@@ -7,6 +7,6 @@ export declare function useChatSession(client: AcpClient | null, initialSessionI
7
7
  sessionId: string | null;
8
8
  connect: () => Promise<void>;
9
9
  loadSession: (sessionIdToLoad: string) => Promise<void>;
10
- startSession: () => Promise<void>;
10
+ startSession: () => Promise<string | null>;
11
11
  disconnect: () => Promise<void>;
12
12
  };
@@ -149,11 +149,12 @@ export function useChatSession(client, initialSessionId) {
149
149
  ]);
150
150
  /**
151
151
  * Start a new session
152
+ * @returns The new session ID, or null if creation failed
152
153
  */
153
154
  const startSession = useCallback(async () => {
154
155
  if (!client) {
155
156
  setError("No client available");
156
- return;
157
+ return null;
157
158
  }
158
159
  try {
159
160
  const id = await client.startSession();
@@ -166,10 +167,12 @@ export function useChatSession(client, initialSessionId) {
166
167
  url.searchParams.set("session", id);
167
168
  window.history.pushState({}, "", url.toString());
168
169
  }
170
+ return id;
169
171
  }
170
172
  catch (error) {
171
173
  const message = error instanceof Error ? error.message : String(error);
172
174
  setError(message);
175
+ return null;
173
176
  }
174
177
  }, [client, setSessionId, setError, clearMessages, resetTokens]);
175
178
  /**
@@ -205,12 +208,6 @@ export function useChatSession(client, initialSessionId) {
205
208
  connect();
206
209
  }
207
210
  }, [client, connectionStatus, initialSessionId, connect, loadSession]);
208
- // Auto-start new session after connecting (only if no initial session)
209
- useEffect(() => {
210
- if (connectionStatus === "connected" && !sessionId && !initialSessionId) {
211
- startSession();
212
- }
213
- }, [connectionStatus, sessionId, initialSessionId, startSession]);
214
211
  return {
215
212
  connectionStatus,
216
213
  sessionId,
@@ -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;
@@ -0,0 +1,24 @@
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;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Browser-compatible logger
3
+ * Outputs structured JSON logs to console with color-coding
4
+ */
5
+ const LOG_LEVELS = {
6
+ trace: 0,
7
+ debug: 1,
8
+ info: 2,
9
+ warn: 3,
10
+ error: 4,
11
+ fatal: 5,
12
+ };
13
+ const _LOG_COLORS = {
14
+ trace: "#6B7280", // gray
15
+ debug: "#3B82F6", // blue
16
+ info: "#10B981", // green
17
+ warn: "#F59E0B", // orange
18
+ error: "#EF4444", // red
19
+ fatal: "#DC2626", // dark red
20
+ };
21
+ const LOG_STYLES = {
22
+ trace: "color: #6B7280",
23
+ debug: "color: #3B82F6; font-weight: bold",
24
+ info: "color: #10B981; font-weight: bold",
25
+ warn: "color: #F59E0B; font-weight: bold",
26
+ error: "color: #EF4444; font-weight: bold",
27
+ fatal: "color: #DC2626; font-weight: bold; background: #FEE2E2",
28
+ };
29
+ export class Logger {
30
+ service;
31
+ minLevel;
32
+ constructor(service, minLevel = "debug") {
33
+ this.service = service;
34
+ this.minLevel = minLevel;
35
+ // In production, suppress trace and debug logs
36
+ if (typeof process !== "undefined" &&
37
+ process.env?.NODE_ENV === "production") {
38
+ this.minLevel = "info";
39
+ }
40
+ }
41
+ shouldLog(level) {
42
+ return LOG_LEVELS[level] >= LOG_LEVELS[this.minLevel];
43
+ }
44
+ log(level, message, metadata) {
45
+ if (!this.shouldLog(level)) {
46
+ return;
47
+ }
48
+ const entry = {
49
+ timestamp: new Date().toISOString(),
50
+ level,
51
+ service: this.service,
52
+ message,
53
+ ...(metadata && { metadata }),
54
+ };
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
+ }
76
+ }
77
+ // Also log the structured JSON for debugging
78
+ if (level === "fatal" || level === "error") {
79
+ console.debug("Structured log:", JSON.stringify(entry));
80
+ }
81
+ }
82
+ trace(message, metadata) {
83
+ this.log("trace", message, metadata);
84
+ }
85
+ debug(message, metadata) {
86
+ this.log("debug", message, metadata);
87
+ }
88
+ info(message, metadata) {
89
+ this.log("info", message, metadata);
90
+ }
91
+ warn(message, metadata) {
92
+ this.log("warn", message, metadata);
93
+ }
94
+ error(message, metadata) {
95
+ this.log("error", message, metadata);
96
+ }
97
+ fatal(message, metadata) {
98
+ this.log("fatal", message, metadata);
99
+ }
100
+ }
101
+ /**
102
+ * Create a logger instance for a service
103
+ * @param service - Service name (e.g., "gui", "http-agent", "tui")
104
+ * @param minLevel - Minimum log level to display (default: "debug")
105
+ */
106
+ export function createLogger(service, minLevel = "debug") {
107
+ return new Logger(service, minLevel);
108
+ }
@@ -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 gap-6", className), ...props, children: [_jsx("h3", { className: "text-heading-4 text-text-primary", children: title }), _jsx("p", { className: "text-subheading text-text-secondary max-w-prose", children: description }), (guideUrl || onGuideClick) && (_jsxs("button", { type: "button", onClick: handleGuideClick, className: "flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-accent transition-colors", children: [_jsx("span", { className: "text-paragraph-sm font-medium leading-normal text-text-primary", children: guideText }), _jsx(ChevronRight, { className: "size-4 text-text-primary" })] })), 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), 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("-")))) })] }))] }));
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";
@@ -8,6 +8,10 @@ export interface ChatInputRootProps extends Omit<React.FormHTMLAttributes<HTMLFo
8
8
  * Either client or value/onChange/onSubmit must be provided
9
9
  */
10
10
  client?: AcpClient | null;
11
+ /**
12
+ * Start session function (required when using client)
13
+ */
14
+ startSession?: () => Promise<string | null>;
11
15
  /**
12
16
  * Input value (legacy prop-based pattern)
13
17
  * Either client or value/onChange/onSubmit must be provided
@@ -15,18 +15,23 @@ const useChatInputContext = () => {
15
15
  }
16
16
  return context;
17
17
  };
18
- const ChatInputRoot = React.forwardRef(({ client, value: valueProp, onChange: onChangeProp, onSubmit: onSubmitProp, disabled = false, isSubmitting: isSubmittingProp, submitOnEnter = true, className, children, ...props }, ref) => {
18
+ const ChatInputRoot = React.forwardRef(({ client, startSession, value: valueProp, onChange: onChangeProp, onSubmit: onSubmitProp, disabled = false, isSubmitting: isSubmittingProp, submitOnEnter = true, className, children, ...props }, ref) => {
19
19
  const textareaRef = React.useRef(null);
20
+ // Provide a dummy startSession function if not provided (for React hooks rule)
21
+ const dummyStartSession = React.useCallback(async () => Promise.resolve(null), []);
20
22
  // Always call hooks unconditionally (React rules)
21
- const hookData = useCoreChatInput(client ?? null);
23
+ const hookData = useCoreChatInput(client ?? null, startSession ?? dummyStartSession);
22
24
  const storeIsStreaming = useChatStore((state) => state.isStreaming);
23
25
  // Choose data source based on whether client is provided
24
- const value = hookData ? hookData.value : valueProp || "";
25
- const onChange = hookData ? hookData.onChange : onChangeProp || (() => { });
26
- const onSubmit = hookData
26
+ const useHookData = client && startSession;
27
+ const value = useHookData ? hookData.value : valueProp || "";
28
+ const onChange = useHookData
29
+ ? hookData.onChange
30
+ : onChangeProp || (() => { });
31
+ const onSubmit = useHookData
27
32
  ? hookData.onSubmit
28
33
  : onSubmitProp || (async () => { });
29
- const isSubmitting = hookData
34
+ const isSubmitting = useHookData
30
35
  ? hookData.isSubmitting || storeIsStreaming
31
36
  : isSubmittingProp || false;
32
37
  // Command menu state
@@ -201,7 +206,7 @@ const ChatInputField = React.forwardRef(({ asChild = false, className, onKeyDown
201
206
  if (asChild && React.isValidElement(children)) {
202
207
  return React.cloneElement(children, fieldProps);
203
208
  }
204
- return (_jsx("textarea", { ...fieldProps, className: cn("w-full resize-none rounded-none border-none p-4 shadow-none", "outline-none ring-0 field-sizing-content max-h-[6lh]", "bg-transparent dark:bg-transparent focus-visible:ring-0", "text-paragraph-sm placeholder:text-muted-foreground", "disabled:cursor-not-allowed disabled:opacity-50", className) }));
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) }));
205
210
  });
206
211
  ChatInputField.displayName = "ChatInput.Field";
207
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
- checkScrollPosition();
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
- // Check scroll position on mount and when children change
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
- checkScrollPosition();
107
+ if (!isAutoScrollingRef.current) {
108
+ const isAtBottom = checkScrollPosition();
109
+ wasAtBottomRef.current = isAtBottom;
110
+ }
74
111
  }, [checkScrollPosition]);
75
- 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, ...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, 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" }) }))] }));
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: 15, maxSize: 50, children: _jsx("div", { ref: ref, className: cn(
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: TodoItem[];
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> {