@timetotest/cli 0.2.3 → 0.3.0

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 (200) hide show
  1. package/README.md +49 -40
  2. package/dist/bin/ttt.js +0 -2
  3. package/dist/bin/ttt.js.map +1 -1
  4. package/dist/package.json +13 -6
  5. package/dist/src/commands/chat/ChatApp.js +270 -100
  6. package/dist/src/commands/chat/ChatApp.js.map +1 -1
  7. package/dist/src/commands/chat/components/Banner.js +1 -1
  8. package/dist/src/commands/chat/components/ChatInput.js +97 -36
  9. package/dist/src/commands/chat/components/ChatInput.js.map +1 -1
  10. package/dist/src/commands/chat/components/ChatMessage.js +102 -0
  11. package/dist/src/commands/chat/components/ChatMessage.js.map +1 -0
  12. package/dist/src/commands/chat/components/MessageBubble.js +2 -1
  13. package/dist/src/commands/chat/components/MessageBubble.js.map +1 -1
  14. package/dist/src/commands/chat/components/PermissionPrompt.js +92 -0
  15. package/dist/src/commands/chat/components/PermissionPrompt.js.map +1 -0
  16. package/dist/src/commands/chat/components/StatusIndicator.js +21 -5
  17. package/dist/src/commands/chat/components/StatusIndicator.js.map +1 -1
  18. package/dist/src/commands/chat/components/ToolCallDisplay.js +141 -0
  19. package/dist/src/commands/chat/components/ToolCallDisplay.js.map +1 -0
  20. package/dist/src/commands/chat-ink.js +389 -61
  21. package/dist/src/commands/chat-ink.js.map +1 -1
  22. package/dist/src/commands/login.js +5 -5
  23. package/dist/src/commands/login.js.map +1 -1
  24. package/dist/src/commands/test.js +14 -194
  25. package/dist/src/commands/test.js.map +1 -1
  26. package/dist/src/lib/__tests__/code-mode-integration.test.js +381 -0
  27. package/dist/src/lib/__tests__/code-mode-integration.test.js.map +1 -0
  28. package/dist/src/lib/__tests__/config-manager.test.js +81 -0
  29. package/dist/src/lib/__tests__/config-manager.test.js.map +1 -0
  30. package/dist/src/lib/__tests__/mode-persistence-integration.test.js +75 -0
  31. package/dist/src/lib/__tests__/mode-persistence-integration.test.js.map +1 -0
  32. package/dist/src/lib/__tests__/permission-flow-integration.test.js +145 -0
  33. package/dist/src/lib/__tests__/permission-flow-integration.test.js.map +1 -0
  34. package/dist/src/lib/__tests__/permissions.test.js +132 -0
  35. package/dist/src/lib/__tests__/permissions.test.js.map +1 -0
  36. package/dist/src/lib/agent-orchestrator.js +263 -4
  37. package/dist/src/lib/agent-orchestrator.js.map +1 -1
  38. package/dist/src/lib/config.js +40 -0
  39. package/dist/src/lib/config.js.map +1 -1
  40. package/dist/src/lib/context-compactor.js +310 -0
  41. package/dist/src/lib/context-compactor.js.map +1 -0
  42. package/dist/src/lib/http.js +8 -0
  43. package/dist/src/lib/http.js.map +1 -1
  44. package/dist/src/lib/local-tools/code/__tests__/grep-search.test.js +146 -0
  45. package/dist/src/lib/local-tools/code/__tests__/grep-search.test.js.map +1 -0
  46. package/dist/src/lib/local-tools/code/__tests__/list-directory.test.js +192 -0
  47. package/dist/src/lib/local-tools/code/__tests__/list-directory.test.js.map +1 -0
  48. package/dist/src/lib/local-tools/code/__tests__/read-file.test.js +169 -0
  49. package/dist/src/lib/local-tools/code/__tests__/read-file.test.js.map +1 -0
  50. package/dist/src/lib/local-tools/code/__tests__/run-command.test.js +101 -0
  51. package/dist/src/lib/local-tools/code/__tests__/run-command.test.js.map +1 -0
  52. package/dist/src/lib/local-tools/code/__tests__/search-files.test.js +191 -0
  53. package/dist/src/lib/local-tools/code/__tests__/search-files.test.js.map +1 -0
  54. package/dist/src/lib/local-tools/code/grep-search.js +404 -0
  55. package/dist/src/lib/local-tools/code/grep-search.js.map +1 -0
  56. package/dist/src/lib/local-tools/code/index.js +11 -0
  57. package/dist/src/lib/local-tools/code/index.js.map +1 -0
  58. package/dist/src/lib/local-tools/code/list-directory.js +276 -0
  59. package/dist/src/lib/local-tools/code/list-directory.js.map +1 -0
  60. package/dist/src/lib/local-tools/code/read-file.js +301 -0
  61. package/dist/src/lib/local-tools/code/read-file.js.map +1 -0
  62. package/dist/src/lib/local-tools/code/run-command.js +235 -0
  63. package/dist/src/lib/local-tools/code/run-command.js.map +1 -0
  64. package/dist/src/lib/local-tools/code/search-files.js +297 -0
  65. package/dist/src/lib/local-tools/code/search-files.js.map +1 -0
  66. package/dist/src/lib/local-tools/code/types.js +6 -0
  67. package/dist/src/lib/local-tools/code/types.js.map +1 -0
  68. package/dist/src/lib/local-tools/ui/playwright-mcp.js +1 -1
  69. package/dist/src/lib/permissions.js +94 -0
  70. package/dist/src/lib/permissions.js.map +1 -0
  71. package/dist/src/lib/prompts/builder.js +13 -10
  72. package/dist/src/lib/prompts/builder.js.map +1 -1
  73. package/dist/src/lib/prompts/templates.js +78 -0
  74. package/dist/src/lib/prompts/templates.js.map +1 -1
  75. package/dist/src/lib/session-manager.js.map +1 -1
  76. package/dist/src/lib/testing-mode.js +2 -2
  77. package/dist/src/lib/testing-mode.js.map +1 -1
  78. package/dist/src/lib/tool-executor.js +131 -2
  79. package/dist/src/lib/tool-executor.js.map +1 -1
  80. package/dist/src/lib/tool-registry.js +171 -3
  81. package/dist/src/lib/tool-registry.js.map +1 -1
  82. package/dist/src/lib/tool-result-pruner.js +4 -4
  83. package/dist/src/lib/tool-result-pruner.js.map +1 -1
  84. package/dist/src/lib/tui/ink/components/AppFrame.js +17 -0
  85. package/dist/src/lib/tui/ink/components/AppFrame.js.map +1 -0
  86. package/dist/src/lib/tui/ink/components/CommandPalette.js +24 -0
  87. package/dist/src/lib/tui/ink/components/CommandPalette.js.map +1 -0
  88. package/dist/src/lib/tui/ink/components/Pill.js +19 -0
  89. package/dist/src/lib/tui/ink/components/Pill.js.map +1 -0
  90. package/dist/src/lib/tui/ink/components/TimetoTestLogo.js +30 -0
  91. package/dist/src/lib/tui/ink/components/TimetoTestLogo.js.map +1 -0
  92. package/dist/src/lib/tui/ink/theme.js +28 -0
  93. package/dist/src/lib/tui/ink/theme.js.map +1 -0
  94. package/dist/src/lib/tui/interactive-chat.js +35 -35
  95. package/dist/src/lib/tui/interactive-chat.js.map +1 -1
  96. package/dist/src/lib/tui/print.js +18 -18
  97. package/dist/src/lib/tui/print.js.map +1 -1
  98. package/dist/src/lib/tui/prompt.js +3 -3
  99. package/dist/src/lib/tui/prompt.js.map +1 -1
  100. package/dist/src/lib/tui/status.js +1 -1
  101. package/dist/src/lib/tui/status.js.map +1 -1
  102. package/dist/src/lib/update.js +10 -10
  103. package/dist/src/lib/update.js.map +1 -1
  104. package/package.json +13 -6
  105. package/dist/src/commands/ask/AskApp.js +0 -121
  106. package/dist/src/commands/ask/AskApp.js.map +0 -1
  107. package/dist/src/commands/ask/components/AssistantResponse.js +0 -31
  108. package/dist/src/commands/ask/components/AssistantResponse.js.map +0 -1
  109. package/dist/src/commands/ask/components/Banner.js +0 -15
  110. package/dist/src/commands/ask/components/Banner.js.map +0 -1
  111. package/dist/src/commands/ask/components/ChatInput.js +0 -93
  112. package/dist/src/commands/ask/components/ChatInput.js.map +0 -1
  113. package/dist/src/commands/ask/components/Divider.js +0 -17
  114. package/dist/src/commands/ask/components/Divider.js.map +0 -1
  115. package/dist/src/commands/ask/components/IntroTips.js +0 -19
  116. package/dist/src/commands/ask/components/IntroTips.js.map +0 -1
  117. package/dist/src/commands/ask/components/MessageBubble.js +0 -47
  118. package/dist/src/commands/ask/components/MessageBubble.js.map +0 -1
  119. package/dist/src/commands/ask/components/SessionInfo.js +0 -20
  120. package/dist/src/commands/ask/components/SessionInfo.js.map +0 -1
  121. package/dist/src/commands/ask/components/StatusIndicator.js +0 -67
  122. package/dist/src/commands/ask/components/StatusIndicator.js.map +0 -1
  123. package/dist/src/commands/ask-ink.js +0 -380
  124. package/dist/src/commands/ask-ink.js.map +0 -1
  125. package/dist/src/commands/ask.js +0 -991
  126. package/dist/src/commands/ask.js.map +0 -1
  127. package/dist/src/commands/chat/components/Divider.js +0 -7
  128. package/dist/src/commands/chat/components/Divider.js.map +0 -1
  129. package/dist/src/commands/chat/components/SessionInfo.js +0 -11
  130. package/dist/src/commands/chat/components/SessionInfo.js.map +0 -1
  131. package/dist/src/commands/chat.js +0 -82
  132. package/dist/src/commands/chat.js.map +0 -1
  133. package/dist/src/commands/start-test.js +0 -119
  134. package/dist/src/commands/start-test.js.map +0 -1
  135. package/dist/src/commands/stream.js +0 -17
  136. package/dist/src/commands/stream.js.map +0 -1
  137. package/dist/src/lib/legacy-chat-runner.js +0 -37
  138. package/dist/src/lib/legacy-chat-runner.js.map +0 -1
  139. package/dist/src/lib/local-tools/ui/click-element.js +0 -105
  140. package/dist/src/lib/local-tools/ui/click-element.js.map +0 -1
  141. package/dist/src/lib/local-tools/ui/dom-rag.js +0 -201
  142. package/dist/src/lib/local-tools/ui/dom-rag.js.map +0 -1
  143. package/dist/src/lib/local-tools/ui/find-element.js +0 -31
  144. package/dist/src/lib/local-tools/ui/find-element.js.map +0 -1
  145. package/dist/src/lib/local-tools/ui/hover-element.js +0 -94
  146. package/dist/src/lib/local-tools/ui/hover-element.js.map +0 -1
  147. package/dist/src/lib/local-tools/ui/manage-tab.js +0 -65
  148. package/dist/src/lib/local-tools/ui/manage-tab.js.map +0 -1
  149. package/dist/src/lib/local-tools/ui/navigate.js +0 -35
  150. package/dist/src/lib/local-tools/ui/navigate.js.map +0 -1
  151. package/dist/src/lib/local-tools/ui/page-discovery.js +0 -32
  152. package/dist/src/lib/local-tools/ui/page-discovery.js.map +0 -1
  153. package/dist/src/lib/local-tools/ui/screenshot.js +0 -19
  154. package/dist/src/lib/local-tools/ui/screenshot.js.map +0 -1
  155. package/dist/src/lib/local-tools/ui/search-interactive-elements.js +0 -18
  156. package/dist/src/lib/local-tools/ui/search-interactive-elements.js.map +0 -1
  157. package/dist/src/lib/local-tools/ui/selector-resolver.js +0 -153
  158. package/dist/src/lib/local-tools/ui/selector-resolver.js.map +0 -1
  159. package/dist/src/lib/local-tools/ui/type-text.js +0 -40
  160. package/dist/src/lib/local-tools/ui/type-text.js.map +0 -1
  161. package/dist/src/lib/tui/components/AskIntro.js +0 -6
  162. package/dist/src/lib/tui/components/AskIntro.js.map +0 -1
  163. package/dist/src/lib/tui/components/Banner.js +0 -15
  164. package/dist/src/lib/tui/components/Banner.js.map +0 -1
  165. package/dist/src/lib/tui/components/Divider.js +0 -17
  166. package/dist/src/lib/tui/components/Divider.js.map +0 -1
  167. package/dist/src/lib/tui/components/EventLine.js +0 -110
  168. package/dist/src/lib/tui/components/EventLine.js.map +0 -1
  169. package/dist/src/lib/tui/components/Header.js +0 -15
  170. package/dist/src/lib/tui/components/Header.js.map +0 -1
  171. package/dist/src/lib/tui/components/InputBox.js +0 -9
  172. package/dist/src/lib/tui/components/InputBox.js.map +0 -1
  173. package/dist/src/lib/tui/components/Mapping.js +0 -8
  174. package/dist/src/lib/tui/components/Mapping.js.map +0 -1
  175. package/dist/src/lib/tui/components/ProjectList.js +0 -6
  176. package/dist/src/lib/tui/components/ProjectList.js.map +0 -1
  177. package/dist/src/lib/tui/components/Spinner.js +0 -20
  178. package/dist/src/lib/tui/components/Spinner.js.map +0 -1
  179. package/dist/src/lib/tui/components/StatusBanner.js +0 -12
  180. package/dist/src/lib/tui/components/StatusBanner.js.map +0 -1
  181. package/dist/src/lib/tui/components/StatusBar.js +0 -11
  182. package/dist/src/lib/tui/components/StatusBar.js.map +0 -1
  183. package/dist/src/lib/tui/components/UserBubble.js +0 -6
  184. package/dist/src/lib/tui/components/UserBubble.js.map +0 -1
  185. package/dist/src/lib/tui/components/index.js +0 -16
  186. package/dist/src/lib/tui/components/index.js.map +0 -1
  187. package/dist/src/lib/tui/ink-print.js +0 -41
  188. package/dist/src/lib/tui/ink-print.js.map +0 -1
  189. package/dist/src/test-agent-flow.js +0 -148
  190. package/dist/src/test-agent-flow.js.map +0 -1
  191. package/dist/src/test-browser-session.js +0 -152
  192. package/dist/src/test-browser-session.js.map +0 -1
  193. package/dist/src/test-browser-snapshot.js +0 -187
  194. package/dist/src/test-browser-snapshot.js.map +0 -1
  195. package/dist/src/test-snapshot-detailed.js +0 -219
  196. package/dist/src/test-snapshot-detailed.js.map +0 -1
  197. package/dist/src/test-snapshot-simple.js +0 -85
  198. package/dist/src/test-snapshot-simple.js.map +0 -1
  199. package/dist/src/test-snapshot-tabs.js +0 -110
  200. package/dist/src/test-snapshot-tabs.js.map +0 -1
@@ -7,19 +7,11 @@ import chalk from "chalk";
7
7
  import React from "react";
8
8
  import { render } from "ink";
9
9
  import { AgentOrchestrator } from "../lib/agent-orchestrator.js";
10
- import { resolveApiUrl, getAuthToken } from "../lib/config.js";
11
- import { getToolDescription } from "../lib/tool-descriptions.js";
10
+ import { resolveApiUrl, getAuthToken, configManager } from "../lib/config.js";
12
11
  import { ChatApp } from "./chat/ChatApp.js";
13
12
  import { performInteractiveLogin } from "./login.js";
14
13
  import { resolveTestingMode } from "../lib/testing-mode.js";
15
- export const chatInk = new Command()
16
- .name("chat")
17
- .description("Start an interactive chat session with the test agent (INK UI)")
18
- .option("--mode <mode>", "Testing mode: ui or api", "ui")
19
- .option("--base-url <url>", "Base URL for UI testing", process.env.TTT_BASE_URL)
20
- .option("--api-base-url <url>", "Base URL for API testing", process.env.TTT_API_BASE_URL)
21
- .option("--session <id>", "Resume existing session by ID")
22
- .action(async (options) => {
14
+ export async function runChatInk(options) {
23
15
  try {
24
16
  const apiUrl = resolveApiUrl();
25
17
  let token = getAuthToken();
@@ -71,19 +63,27 @@ export const chatInk = new Command()
71
63
  }
72
64
  }
73
65
  else {
74
- console.log(chalk.gray(`ℹ️ Unable to fetch user info: ${error?.message || error}`));
66
+ console.error(chalk.gray(`ℹ️ Unable to fetch user info: ${error?.message || error}`));
75
67
  }
76
68
  }
77
69
  // Determine mode (local is default)
78
70
  const mode = "local";
79
- console.log(chalk.green("✓ Running in LOCAL mode"));
71
+ // Load saved mode preference, use --mode flag if provided, otherwise fall back to saved or default
80
72
  let testingMode;
81
73
  try {
82
- testingMode = resolveTestingMode(options.mode ?? "ui", {
83
- defaultMode: "ui",
84
- strict: true,
85
- contextLabel: "--mode",
86
- });
74
+ if (options.mode) {
75
+ // User explicitly provided --mode flag
76
+ testingMode = resolveTestingMode(options.mode, {
77
+ defaultMode: "ui",
78
+ strict: true,
79
+ contextLabel: "--mode",
80
+ });
81
+ }
82
+ else {
83
+ // Try to load saved mode preference
84
+ const savedMode = await configManager.getLastMode();
85
+ testingMode = savedMode ?? "ui";
86
+ }
87
87
  }
88
88
  catch (error) {
89
89
  console.error(chalk.red(error?.message || String(error)));
@@ -102,6 +102,17 @@ export const chatInk = new Command()
102
102
  function getAppInterface() {
103
103
  return globalThis.__chatAppInterface;
104
104
  }
105
+ // Set up permission prompt handler - delegates to ChatApp UI
106
+ const permissionPromptFn = async (request) => {
107
+ const appInterface = getAppInterface();
108
+ if (!appInterface?.promptPermission) {
109
+ // Fallback: deny if UI not available
110
+ return "deny";
111
+ }
112
+ return appInterface.promptPermission(request);
113
+ };
114
+ // Wire up permission handler to orchestrator's tool executor
115
+ orchestrator.setPermissionPromptFn(permissionPromptFn);
105
116
  function handleUserCancel() {
106
117
  const appInterface = getAppInterface();
107
118
  const wasCancelled = userCancelled;
@@ -118,46 +129,61 @@ export const chatInk = new Command()
118
129
  orchestrator.cancel();
119
130
  }
120
131
  // Set up event listeners for tool execution with frontend-style display
121
- orchestrator.on("assistant_reasoning", (data) => {
132
+ const onAssistantReasoning = (data) => {
122
133
  if (userCancelled) {
123
134
  return;
124
135
  }
125
136
  const appInterface = getAppInterface();
126
137
  const reasoning = data.data?.reasoning || data.message;
127
138
  if (reasoning && reasoning.trim()) {
128
- // Add reasoning as a system message to show in history
139
+ // Add reasoning as a system message with special formatting
129
140
  appInterface?.addMessage({
130
141
  id: `reasoning-${Date.now()}`,
131
142
  type: "system",
132
- content: `${reasoning}`,
143
+ content: reasoning,
144
+ metadata: { isReasoning: true },
133
145
  });
134
146
  }
135
- });
136
- orchestrator.on("tool_start", (data) => {
147
+ };
148
+ const onToolStart = (data) => {
137
149
  if (userCancelled) {
138
150
  return;
139
151
  }
140
152
  const appInterface = getAppInterface();
141
153
  const toolName = data.data?.tool || data.tool;
142
- const description = getToolDescription(toolName);
143
- // Add tool event as a system message to show in history
154
+ const toolArgs = data.data?.arguments || {};
155
+ // Add tool call as a system message with tool metadata
144
156
  appInterface?.addMessage({
145
157
  id: `tool-${Date.now()}`,
146
158
  type: "system",
147
- content: `🔧 ${toolName.replace(/_/g, " ")} - ${description}`,
159
+ content: "", // Content will be rendered by ToolCallDisplay
160
+ metadata: {
161
+ isTool: true,
162
+ toolName,
163
+ toolArgs,
164
+ isLoading: true,
165
+ },
148
166
  });
149
167
  appInterface?.setStatus({
150
168
  text: "Agent thinking...",
151
169
  type: "loading",
152
170
  });
153
- });
154
- orchestrator.on("tool_result", () => {
171
+ };
172
+ const onToolResult = (data) => {
155
173
  if (userCancelled) {
156
174
  return;
157
175
  }
158
- // Tool completion is intentionally silent now.
159
- });
160
- orchestrator.on("agent_cancelled", () => {
176
+ const appInterface = getAppInterface();
177
+ const toolName = data.data?.tool || data.tool;
178
+ const result = data.data?.result;
179
+ // Update the last tool message with the result
180
+ appInterface?.updateLastToolMessage?.({
181
+ toolName,
182
+ result,
183
+ isLoading: false,
184
+ });
185
+ };
186
+ const onAgentCancelled = () => {
161
187
  if (userCancelled) {
162
188
  return;
163
189
  }
@@ -168,7 +194,18 @@ export const chatInk = new Command()
168
194
  type: "system",
169
195
  content: "🛑 Cancelled — the assistant has stopped. Type whenever you're ready.",
170
196
  });
171
- });
197
+ };
198
+ orchestrator.on("assistant_reasoning", onAssistantReasoning);
199
+ orchestrator.on("tool_start", onToolStart);
200
+ orchestrator.on("tool_result", onToolResult);
201
+ orchestrator.on("agent_cancelled", onAgentCancelled);
202
+ const cleanupOrchestratorListeners = () => {
203
+ // EventEmitter supports `off` in modern Node, but be tolerant.
204
+ (orchestrator.off ?? orchestrator.removeListener).call(orchestrator, "assistant_reasoning", onAssistantReasoning);
205
+ (orchestrator.off ?? orchestrator.removeListener).call(orchestrator, "tool_start", onToolStart);
206
+ (orchestrator.off ?? orchestrator.removeListener).call(orchestrator, "tool_result", onToolResult);
207
+ (orchestrator.off ?? orchestrator.removeListener).call(orchestrator, "agent_cancelled", onAgentCancelled);
208
+ };
172
209
  // Initialize context
173
210
  const chatContext = {
174
211
  sessionId: orchestrator.getSessionId(),
@@ -188,8 +225,117 @@ export const chatInk = new Command()
188
225
  chatContext.toolCount = history.filter((msg) => msg.role === "tool").length;
189
226
  };
190
227
  updateContextFromOrchestrator();
191
- // Define slash commands
228
+ // Define slash commands (alphabetically sorted)
192
229
  const slashCommands = [
230
+ {
231
+ name: "cancel",
232
+ description: "Alias for /stop",
233
+ handler: async () => {
234
+ handleUserCancel();
235
+ return "continue";
236
+ },
237
+ },
238
+ {
239
+ name: "clear",
240
+ description: "Clear the transcript visually",
241
+ handler: async () => "continue",
242
+ },
243
+ {
244
+ name: "bugs",
245
+ description: "List all bugs identified in this session",
246
+ handler: async (args, orchestrator) => {
247
+ const history = orchestrator.getConversationHistory();
248
+ const findings = history
249
+ .filter((msg) => msg.role === "tool" && msg.toolCall?.name === "generate_document")
250
+ .map((msg) => {
251
+ try {
252
+ const res = JSON.parse(msg.content);
253
+ return res.findings || [];
254
+ }
255
+ catch {
256
+ return [];
257
+ }
258
+ })
259
+ .flat();
260
+ const appInterface = getAppInterface();
261
+ if (findings.length === 0) {
262
+ appInterface?.addMessage({
263
+ id: Date.now().toString(),
264
+ type: "system",
265
+ content: "No bugs have been formally reported yet. Use /report to generate a report.",
266
+ });
267
+ }
268
+ else {
269
+ const list = findings
270
+ .map((f, i) => `${i + 1}. [${f.severity?.toUpperCase()}] ${f.title}`)
271
+ .join("\n");
272
+ appInterface?.addMessage({
273
+ id: Date.now().toString(),
274
+ type: "system",
275
+ content: `🐞 Identified Bugs:\n${list}`,
276
+ });
277
+ }
278
+ return "continue";
279
+ },
280
+ },
281
+ {
282
+ name: "compact",
283
+ description: "Compact conversation context to manage token limits",
284
+ handler: async () => {
285
+ const stats = orchestrator.getContextStats();
286
+ const appInterface = getAppInterface();
287
+ if (!stats.needsCompaction) {
288
+ appInterface?.addMessage({
289
+ id: Date.now().toString(),
290
+ type: "system",
291
+ content: `Context: ${stats.totalMessages} messages, ${stats.tokens.toLocaleString()} tokens (${stats.utilizationPercent.toFixed(1)}% of limit), ${stats.conversationTurns} turns. No compaction needed.`,
292
+ });
293
+ return "continue";
294
+ }
295
+ const result = await orchestrator.compactContext();
296
+ appInterface?.addMessage({
297
+ id: Date.now().toString(),
298
+ type: "system",
299
+ content: `✅ Context compacted: ${result.stats.removedCount} messages removed\n` +
300
+ `Before: ${result.stats.tokensBeforeCompaction.toLocaleString()} tokens\n` +
301
+ `After: ${result.stats.tokensAfterCompaction.toLocaleString()} tokens\n` +
302
+ `Strategy: ${result.stats.strategy}` +
303
+ (result.stats.summaryGenerated
304
+ ? "\n📝 LLM summary generated"
305
+ : ""),
306
+ });
307
+ updateContextFromOrchestrator();
308
+ appInterface?.updateContext(chatContext);
309
+ return "continue";
310
+ },
311
+ },
312
+ {
313
+ name: "exit",
314
+ description: "Exit the session (saves progress)",
315
+ handler: async () => "exit",
316
+ },
317
+ {
318
+ name: "goto",
319
+ description: "Direct the agent to a specific URL",
320
+ handler: async (args, orchestrator) => {
321
+ if (args.length === 0) {
322
+ getAppInterface()?.addMessage({
323
+ id: Date.now().toString(),
324
+ type: "system",
325
+ content: "Usage: /goto <url>",
326
+ });
327
+ return "continue";
328
+ }
329
+ const url = args[0];
330
+ getAppInterface()?.addMessage({
331
+ id: Date.now().toString(),
332
+ type: "system",
333
+ content: `🚀 Navigating to ${url}...`,
334
+ });
335
+ void handleUserMessage(`Navigate to ${url} and tell me what you see.`);
336
+ return "continue";
337
+ },
338
+ },
193
339
  {
194
340
  name: "help",
195
341
  description: "Show available slash commands",
@@ -265,56 +411,96 @@ export const chatInk = new Command()
265
411
  },
266
412
  },
267
413
  {
268
- name: "session",
269
- description: "Display session metadata and storage path",
270
- handler: async () => {
271
- const sessionText = [
272
- `ID: ${chatContext.sessionId}`,
273
- `Path: ${chatContext.sessionPath}`,
274
- `Mode: ${chatContext.mode}`,
275
- `Testing mode: ${chatContext.testingMode}`,
276
- ].join("\n");
414
+ name: "report",
415
+ description: "Force the agent to generate a final report",
416
+ handler: async (args, orchestrator) => {
277
417
  getAppInterface()?.addMessage({
278
418
  id: Date.now().toString(),
279
419
  type: "system",
280
- content: sessionText,
420
+ content: "📑 Generating report based on session history...",
281
421
  });
422
+ void handleUserMessage("Please generate a final report of your findings for this session.");
282
423
  return "continue";
283
424
  },
284
425
  },
285
426
  {
286
- name: "clear",
287
- description: "Clear the terminal and redraw the header",
427
+ name: "scan",
428
+ description: "Instruct the agent to scan the page for interactive elements",
288
429
  handler: async () => {
289
- console.clear();
430
+ getAppInterface()?.addMessage({
431
+ id: Date.now().toString(),
432
+ type: "system",
433
+ content: "🔍 Scanning page for interactive elements...",
434
+ });
435
+ void handleUserMessage("Scan the current page and list all interactive elements you find.");
290
436
  return "continue";
291
437
  },
292
438
  },
293
439
  {
294
- name: "stop",
295
- description: "Stop the assistant (same as ESC)",
440
+ name: "screenshot",
441
+ description: "Force the agent to take a screenshot and show the state",
296
442
  handler: async () => {
297
- handleUserCancel();
443
+ getAppInterface()?.addMessage({
444
+ id: Date.now().toString(),
445
+ type: "system",
446
+ content: "📸 Capturing fresh screenshot...",
447
+ });
448
+ void handleUserMessage("Please take a screenshot of the current page and tell me what's visible.");
298
449
  return "continue";
299
450
  },
300
451
  },
301
452
  {
302
- name: "cancel",
303
- description: "Alias for /stop",
453
+ name: "where",
454
+ description: "Get the current URL and page title from the agent",
304
455
  handler: async () => {
305
- handleUserCancel();
456
+ getAppInterface()?.addMessage({
457
+ id: Date.now().toString(),
458
+ type: "system",
459
+ content: "📍 Checking location...",
460
+ });
461
+ void handleUserMessage("What is the current URL and page title?");
306
462
  return "continue";
307
463
  },
308
464
  },
309
465
  {
310
- name: "exit",
311
- description: "Exit the chat session",
312
- handler: async () => "exit",
466
+ name: "reset",
467
+ description: "Reset session and clear all messages",
468
+ handler: async (args, orchestrator) => {
469
+ orchestrator.cancel();
470
+ getAppInterface()?.addMessage({
471
+ id: Date.now().toString(),
472
+ type: "system",
473
+ content: "🧹 Session reset. History cleared.",
474
+ });
475
+ // Note: In a real app we'd want to tell the orchestrator to reset its internal state too
476
+ return "continue";
477
+ },
313
478
  },
314
479
  {
315
- name: "quit",
316
- description: "Alias for /exit",
317
- handler: async () => "exit",
480
+ name: "session",
481
+ description: "Display session metadata and storage path",
482
+ handler: async () => {
483
+ const sessionText = [
484
+ `ID: ${chatContext.sessionId}`,
485
+ `Path: ${chatContext.sessionPath}`,
486
+ `Mode: ${chatContext.mode}`,
487
+ `Testing mode: ${chatContext.testingMode}`,
488
+ ].join("\n");
489
+ getAppInterface()?.addMessage({
490
+ id: Date.now().toString(),
491
+ type: "system",
492
+ content: sessionText,
493
+ });
494
+ return "continue";
495
+ },
496
+ },
497
+ {
498
+ name: "stop",
499
+ description: "Stop the assistant (same as ESC)",
500
+ handler: async () => {
501
+ handleUserCancel();
502
+ return "continue";
503
+ },
318
504
  },
319
505
  {
320
506
  name: "logout",
@@ -339,6 +525,108 @@ export const chatInk = new Command()
339
525
  return "exit";
340
526
  },
341
527
  },
528
+ {
529
+ name: "model",
530
+ description: "Switch AI model (Pro tier required)",
531
+ handler: async (args, orchestrator, context) => {
532
+ const appInterface = getAppInterface();
533
+ try {
534
+ const response = await http.get("/api/v1/ai/models");
535
+ const data = response.data;
536
+ // Check if user can use premium models
537
+ const availableModels = data.models.filter((m) => m.available);
538
+ const hasPremiumAccess = availableModels.some((m) => m.tier === "premium");
539
+ if (!hasPremiumAccess) {
540
+ appInterface?.addMessage({
541
+ id: Date.now().toString(),
542
+ type: "system",
543
+ content: "❌ Model switching requires Pro tier. Upgrade at https://timetotest.com/pricing",
544
+ });
545
+ return "continue";
546
+ }
547
+ // No args - show available models
548
+ if (args.length === 0) {
549
+ const modelsList = availableModels
550
+ .map((m) => {
551
+ const usesInfo = m.usesRemaining !== null
552
+ ? ` (${m.usesRemaining} free uses left)`
553
+ : "";
554
+ const creditInfo = m.credits > 0
555
+ ? ` - ${m.credits} credit${m.credits !== 1 ? "s" : ""}/msg`
556
+ : "";
557
+ return ` ${m.id} - ${m.name}${creditInfo}${usesInfo}`;
558
+ })
559
+ .join("\n");
560
+ // Show appropriate balance/plan info
561
+ const planTier = data.planTier || "free";
562
+ const isPaidPlan = planTier !== "free";
563
+ const creditBalance = data.creditBalance || 0;
564
+ let balanceInfo = "";
565
+ if (isPaidPlan) {
566
+ // Paid plan users - show plan tier
567
+ balanceInfo = `Plan: ${planTier.charAt(0).toUpperCase() + planTier.slice(1)}`;
568
+ if (creditBalance > 0) {
569
+ balanceInfo += ` | Extra credits: ${creditBalance}`;
570
+ }
571
+ }
572
+ else {
573
+ // Free tier - show credits
574
+ balanceInfo = `Credits: ${creditBalance}`;
575
+ }
576
+ appInterface?.addMessage({
577
+ id: Date.now().toString(),
578
+ type: "system",
579
+ content: `Available Models:\n${modelsList}\n\n${balanceInfo}\n\nUsage: /model <model-id>`,
580
+ });
581
+ return "continue";
582
+ }
583
+ // Model switching
584
+ const modelId = args[0];
585
+ const model = data.models.find((m) => m.id === modelId);
586
+ if (!model || !model.available) {
587
+ appInterface?.addMessage({
588
+ id: Date.now().toString(),
589
+ type: "system",
590
+ content: model
591
+ ? `❌ ${model.name} is unavailable`
592
+ : `❌ Unknown model: ${modelId}`,
593
+ });
594
+ return "continue";
595
+ }
596
+ await orchestrator.setModel(modelId);
597
+ // Show appropriate success message
598
+ const planTier = data.planTier || "free";
599
+ const isPaidPlan = planTier !== "free";
600
+ const creditBalance = data.creditBalance || 0;
601
+ let statusInfo = "";
602
+ if (model.credits > 0) {
603
+ if (isPaidPlan) {
604
+ statusInfo = ` (${model.credits} credit${model.credits !== 1 ? "s" : ""}/msg, ${planTier} plan)`;
605
+ }
606
+ else if (creditBalance > 0) {
607
+ statusInfo = ` (${model.credits} credit${model.credits !== 1 ? "s" : ""}/msg, ${creditBalance} credits remaining)`;
608
+ }
609
+ else {
610
+ statusInfo = ` (${model.credits} credit${model.credits !== 1 ? "s" : ""}/msg)`;
611
+ }
612
+ }
613
+ appInterface?.addMessage({
614
+ id: Date.now().toString(),
615
+ type: "system",
616
+ content: `✅ Switched to ${model.name}${statusInfo}`,
617
+ });
618
+ return "continue";
619
+ }
620
+ catch (error) {
621
+ appInterface?.addMessage({
622
+ id: Date.now().toString(),
623
+ type: "system",
624
+ content: `❌ Failed: ${error?.message || "Unknown error"}`,
625
+ });
626
+ return "continue";
627
+ }
628
+ },
629
+ },
342
630
  ];
343
631
  const commandMap = new Map(slashCommands.map((command) => [command.name, command.handler]));
344
632
  const executeSlashCommand = async (cmd, args) => {
@@ -400,31 +688,71 @@ export const chatInk = new Command()
400
688
  }
401
689
  };
402
690
  const handleExit = () => {
403
- console.log(chalk.green("\n✓ Session saved. Run 'ttt resume' to continue."));
691
+ cleanupOrchestratorListeners();
692
+ console.error(chalk.green("\n✓ Session saved. Run 'ttt resume' to continue."));
404
693
  process.exit(0);
405
694
  };
406
695
  // Prepare slash commands for the UI
407
696
  const slashCommandsForUI = slashCommands.map((cmd) => ({
408
697
  name: cmd.name,
409
698
  description: cmd.description,
410
- value: cmd.name,
699
+ value: `/${cmd.name}`,
411
700
  label: `/${cmd.name}`,
412
701
  }));
413
702
  // Render the INK app
414
- const { waitUntilExit } = render(React.createElement(ChatApp, {
703
+ const { waitUntilExit, clear } = render(React.createElement(ChatApp, {
415
704
  user: userName,
416
705
  context: chatContext,
417
706
  slashCommands: slashCommandsForUI,
418
707
  onUserMessage: handleUserMessage,
419
708
  onSlashCommand: executeSlashCommand,
709
+ onSetTestingMode: async (mode) => {
710
+ orchestrator.setTestingMode(mode);
711
+ updateContextFromOrchestrator();
712
+ getAppInterface()?.updateContext(chatContext);
713
+ // Save mode preference for next session
714
+ await configManager.setLastMode(mode);
715
+ },
420
716
  onExit: handleExit,
421
717
  onCancel: handleUserCancel,
422
- }));
718
+ initialInput: options.initialInput,
719
+ }), {
720
+ stdin: process.stdin,
721
+ stdout: process.stdout,
722
+ debug: false,
723
+ patchConsole: false,
724
+ });
725
+ // Clear and re-render on terminal resize to prevent ghosting
726
+ const handleResize = () => {
727
+ // Clear entire terminal including scrollback
728
+ process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
729
+ // Reset Ink's line counter and force re-render
730
+ clear();
731
+ // Force a re-render by triggering state change
732
+ const appInterface = getAppInterface();
733
+ if (appInterface) {
734
+ appInterface.setStatus({ text: "", type: "info" });
735
+ setTimeout(() => appInterface.clearStatus(), 0);
736
+ }
737
+ };
738
+ process.stdout.on("resize", handleResize);
423
739
  await waitUntilExit();
740
+ process.stdout.off("resize", handleResize);
741
+ cleanupOrchestratorListeners();
424
742
  }
425
743
  catch (error) {
426
744
  console.error(chalk.red(`Error: ${error.message}`));
427
745
  process.exit(1);
428
746
  }
747
+ }
748
+ export const chatInk = new Command()
749
+ .name("chat")
750
+ .description("Start an interactive chat session with the test agent (INK UI). Supports UI testing (browser automation), API testing (HTTP requests), and Code analysis (local file analysis and bug finding).")
751
+ .option("--mode <mode>", "Testing mode: ui, api, or code", "ui")
752
+ .option("--base-url <url>", "Base URL for UI testing", process.env.TTT_BASE_URL)
753
+ .option("--api-base-url <url>", "Base URL for API testing", process.env.TTT_API_BASE_URL)
754
+ .option("--session <id>", "Resume existing session by ID")
755
+ .action(async (options) => {
756
+ await runChatInk(options);
429
757
  });
430
758
  //# sourceMappingURL=chat-ink.js.map