@timetotest/cli 0.2.4 → 0.3.1

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 (114) 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 +8 -3
  5. package/dist/src/commands/chat/ChatApp.js +249 -67
  6. package/dist/src/commands/chat/ChatApp.js.map +1 -1
  7. package/dist/src/commands/chat/OnboardingApp.js +49 -0
  8. package/dist/src/commands/chat/OnboardingApp.js.map +1 -0
  9. package/dist/src/commands/chat/components/Banner.js +1 -1
  10. package/dist/src/commands/chat/components/Banner.js.map +1 -1
  11. package/dist/src/commands/chat/components/ChatInput.js +61 -22
  12. package/dist/src/commands/chat/components/ChatInput.js.map +1 -1
  13. package/dist/src/commands/chat/components/ChatMessage.js +102 -0
  14. package/dist/src/commands/chat/components/ChatMessage.js.map +1 -0
  15. package/dist/src/commands/chat/components/MessageBubble.js +1 -1
  16. package/dist/src/commands/chat/components/MessageBubble.js.map +1 -1
  17. package/dist/src/commands/chat/components/PermissionPrompt.js +92 -0
  18. package/dist/src/commands/chat/components/PermissionPrompt.js.map +1 -0
  19. package/dist/src/commands/chat/components/StatusIndicator.js +21 -5
  20. package/dist/src/commands/chat/components/StatusIndicator.js.map +1 -1
  21. package/dist/src/commands/chat/components/ToolCallDisplay.js +141 -0
  22. package/dist/src/commands/chat/components/ToolCallDisplay.js.map +1 -0
  23. package/dist/src/commands/chat-ink.js +328 -93
  24. package/dist/src/commands/chat-ink.js.map +1 -1
  25. package/dist/src/commands/login.js +5 -5
  26. package/dist/src/commands/login.js.map +1 -1
  27. package/dist/src/commands/test.js +14 -291
  28. package/dist/src/commands/test.js.map +1 -1
  29. package/dist/src/lib/__tests__/code-mode-integration.test.js +381 -0
  30. package/dist/src/lib/__tests__/code-mode-integration.test.js.map +1 -0
  31. package/dist/src/lib/__tests__/config-manager.test.js +81 -0
  32. package/dist/src/lib/__tests__/config-manager.test.js.map +1 -0
  33. package/dist/src/lib/__tests__/mode-persistence-integration.test.js +75 -0
  34. package/dist/src/lib/__tests__/mode-persistence-integration.test.js.map +1 -0
  35. package/dist/src/lib/__tests__/permission-flow-integration.test.js +145 -0
  36. package/dist/src/lib/__tests__/permission-flow-integration.test.js.map +1 -0
  37. package/dist/src/lib/__tests__/permissions.test.js +132 -0
  38. package/dist/src/lib/__tests__/permissions.test.js.map +1 -0
  39. package/dist/src/lib/agent-orchestrator.js +259 -5
  40. package/dist/src/lib/agent-orchestrator.js.map +1 -1
  41. package/dist/src/lib/config.js +40 -0
  42. package/dist/src/lib/config.js.map +1 -1
  43. package/dist/src/lib/context-compactor.js +310 -0
  44. package/dist/src/lib/context-compactor.js.map +1 -0
  45. package/dist/src/lib/http.js +8 -0
  46. package/dist/src/lib/http.js.map +1 -1
  47. package/dist/src/lib/local-tools/code/__tests__/grep-search.test.js +146 -0
  48. package/dist/src/lib/local-tools/code/__tests__/grep-search.test.js.map +1 -0
  49. package/dist/src/lib/local-tools/code/__tests__/list-directory.test.js +192 -0
  50. package/dist/src/lib/local-tools/code/__tests__/list-directory.test.js.map +1 -0
  51. package/dist/src/lib/local-tools/code/__tests__/read-file.test.js +169 -0
  52. package/dist/src/lib/local-tools/code/__tests__/read-file.test.js.map +1 -0
  53. package/dist/src/lib/local-tools/code/__tests__/run-command.test.js +101 -0
  54. package/dist/src/lib/local-tools/code/__tests__/run-command.test.js.map +1 -0
  55. package/dist/src/lib/local-tools/code/__tests__/search-files.test.js +191 -0
  56. package/dist/src/lib/local-tools/code/__tests__/search-files.test.js.map +1 -0
  57. package/dist/src/lib/local-tools/code/grep-search.js +404 -0
  58. package/dist/src/lib/local-tools/code/grep-search.js.map +1 -0
  59. package/dist/src/lib/local-tools/code/index.js +11 -0
  60. package/dist/src/lib/local-tools/code/index.js.map +1 -0
  61. package/dist/src/lib/local-tools/code/list-directory.js +276 -0
  62. package/dist/src/lib/local-tools/code/list-directory.js.map +1 -0
  63. package/dist/src/lib/local-tools/code/read-file.js +301 -0
  64. package/dist/src/lib/local-tools/code/read-file.js.map +1 -0
  65. package/dist/src/lib/local-tools/code/run-command.js +235 -0
  66. package/dist/src/lib/local-tools/code/run-command.js.map +1 -0
  67. package/dist/src/lib/local-tools/code/search-files.js +297 -0
  68. package/dist/src/lib/local-tools/code/search-files.js.map +1 -0
  69. package/dist/src/lib/local-tools/code/types.js +6 -0
  70. package/dist/src/lib/local-tools/code/types.js.map +1 -0
  71. package/dist/src/lib/permissions.js +94 -0
  72. package/dist/src/lib/permissions.js.map +1 -0
  73. package/dist/src/lib/prompts/builder.js +13 -10
  74. package/dist/src/lib/prompts/builder.js.map +1 -1
  75. package/dist/src/lib/prompts/templates.js +78 -0
  76. package/dist/src/lib/prompts/templates.js.map +1 -1
  77. package/dist/src/lib/session-manager.js.map +1 -1
  78. package/dist/src/lib/testing-mode.js +2 -2
  79. package/dist/src/lib/testing-mode.js.map +1 -1
  80. package/dist/src/lib/tool-executor.js +131 -2
  81. package/dist/src/lib/tool-executor.js.map +1 -1
  82. package/dist/src/lib/tool-registry.js +171 -3
  83. package/dist/src/lib/tool-registry.js.map +1 -1
  84. package/dist/src/lib/tool-result-pruner.js +4 -4
  85. package/dist/src/lib/tool-result-pruner.js.map +1 -1
  86. package/dist/src/lib/tui/ink/components/AppFrame.js +17 -0
  87. package/dist/src/lib/tui/ink/components/AppFrame.js.map +1 -0
  88. package/dist/src/lib/tui/ink/components/CommandPalette.js +24 -0
  89. package/dist/src/lib/tui/ink/components/CommandPalette.js.map +1 -0
  90. package/dist/src/lib/tui/ink/components/Pill.js +19 -0
  91. package/dist/src/lib/tui/ink/components/Pill.js.map +1 -0
  92. package/dist/src/lib/tui/ink/components/TimetoTestLogo.js +30 -0
  93. package/dist/src/lib/tui/ink/components/TimetoTestLogo.js.map +1 -0
  94. package/dist/src/lib/tui/ink/theme.js +13 -6
  95. package/dist/src/lib/tui/ink/theme.js.map +1 -1
  96. package/dist/src/lib/tui/interactive-chat.js +35 -35
  97. package/dist/src/lib/tui/interactive-chat.js.map +1 -1
  98. package/dist/src/lib/tui/print.js +18 -18
  99. package/dist/src/lib/tui/print.js.map +1 -1
  100. package/dist/src/lib/tui/prompt.js +3 -3
  101. package/dist/src/lib/tui/prompt.js.map +1 -1
  102. package/dist/src/lib/tui/status.js +1 -1
  103. package/dist/src/lib/tui/status.js.map +1 -1
  104. package/dist/src/lib/update.js +10 -10
  105. package/dist/src/lib/update.js.map +1 -1
  106. package/package.json +8 -3
  107. package/dist/src/commands/start-test.js +0 -180
  108. package/dist/src/commands/start-test.js.map +0 -1
  109. package/dist/src/commands/stream/StreamApp.js +0 -127
  110. package/dist/src/commands/stream/StreamApp.js.map +0 -1
  111. package/dist/src/commands/stream.js +0 -52
  112. package/dist/src/commands/stream.js.map +0 -1
  113. package/dist/src/commands/test/TestRunApp.js +0 -183
  114. package/dist/src/commands/test/TestRunApp.js.map +0 -1
@@ -7,36 +7,44 @@ 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();
26
18
  if (!token) {
27
- try {
28
- await performInteractiveLogin();
29
- // Update token after authentication
30
- token = getAuthToken();
31
- if (!token) {
32
- console.error("❌ Authentication failed");
33
- process.exit(1);
34
- }
35
- }
36
- catch (error) {
37
- console.error(`❌ Authentication failed: ${error?.message || error}`);
38
- process.exit(1);
39
- }
19
+ // Use TUI onboarding instead of direct login
20
+ // We need to dynamically import OnboardingApp to avoid top-level side effects if possible
21
+ const { OnboardingApp } = await import("./chat/OnboardingApp.js");
22
+ // Wrap TUI in a promise to wait for user interaction
23
+ await new Promise((resolve, reject) => {
24
+ const { unmount } = render(React.createElement(OnboardingApp, {
25
+ onLogin: async () => {
26
+ unmount();
27
+ try {
28
+ await performInteractiveLogin();
29
+ // Update token after authentication
30
+ token = getAuthToken();
31
+ if (!token) {
32
+ console.error("❌ Authentication failed");
33
+ process.exit(1);
34
+ }
35
+ resolve();
36
+ }
37
+ catch (error) {
38
+ console.error(`❌ Authentication failed: ${error?.message || error}`);
39
+ process.exit(1);
40
+ }
41
+ },
42
+ onExit: () => {
43
+ unmount();
44
+ process.exit(0);
45
+ },
46
+ }));
47
+ });
40
48
  }
41
49
  // Fetch user information
42
50
  const { createHttpClient } = await import("../lib/http.js");
@@ -71,19 +79,27 @@ export const chatInk = new Command()
71
79
  }
72
80
  }
73
81
  else {
74
- console.log(chalk.gray(`ℹ️ Unable to fetch user info: ${error?.message || error}`));
82
+ console.error(chalk.gray(`ℹ️ Unable to fetch user info: ${error?.message || error}`));
75
83
  }
76
84
  }
77
85
  // Determine mode (local is default)
78
86
  const mode = "local";
79
- console.log(chalk.green("✓ Running in LOCAL mode"));
87
+ // Load saved mode preference, use --mode flag if provided, otherwise fall back to saved or default
80
88
  let testingMode;
81
89
  try {
82
- testingMode = resolveTestingMode(options.mode ?? "ui", {
83
- defaultMode: "ui",
84
- strict: true,
85
- contextLabel: "--mode",
86
- });
90
+ if (options.mode) {
91
+ // User explicitly provided --mode flag
92
+ testingMode = resolveTestingMode(options.mode, {
93
+ defaultMode: "ui",
94
+ strict: true,
95
+ contextLabel: "--mode",
96
+ });
97
+ }
98
+ else {
99
+ // Try to load saved mode preference
100
+ const savedMode = await configManager.getLastMode();
101
+ testingMode = savedMode ?? "ui";
102
+ }
87
103
  }
88
104
  catch (error) {
89
105
  console.error(chalk.red(error?.message || String(error)));
@@ -91,7 +107,7 @@ export const chatInk = new Command()
91
107
  }
92
108
  const config = {
93
109
  apiUrl,
94
- token,
110
+ token: token || undefined,
95
111
  mode: "local",
96
112
  testingMode,
97
113
  baseUrl: options.baseUrl,
@@ -102,6 +118,17 @@ export const chatInk = new Command()
102
118
  function getAppInterface() {
103
119
  return globalThis.__chatAppInterface;
104
120
  }
121
+ // Set up permission prompt handler - delegates to ChatApp UI
122
+ const permissionPromptFn = async (request) => {
123
+ const appInterface = getAppInterface();
124
+ if (!appInterface?.promptPermission) {
125
+ // Fallback: deny if UI not available
126
+ return "deny";
127
+ }
128
+ return appInterface.promptPermission(request);
129
+ };
130
+ // Wire up permission handler to orchestrator's tool executor
131
+ orchestrator.setPermissionPromptFn(permissionPromptFn);
105
132
  function handleUserCancel() {
106
133
  const appInterface = getAppInterface();
107
134
  const wasCancelled = userCancelled;
@@ -125,11 +152,12 @@ export const chatInk = new Command()
125
152
  const appInterface = getAppInterface();
126
153
  const reasoning = data.data?.reasoning || data.message;
127
154
  if (reasoning && reasoning.trim()) {
128
- // Add reasoning as a system message to show in history
155
+ // Add reasoning as a system message with special formatting
129
156
  appInterface?.addMessage({
130
157
  id: `reasoning-${Date.now()}`,
131
158
  type: "system",
132
- content: `${reasoning}`,
159
+ content: reasoning,
160
+ metadata: { isReasoning: true },
133
161
  });
134
162
  }
135
163
  };
@@ -139,23 +167,37 @@ export const chatInk = new Command()
139
167
  }
140
168
  const appInterface = getAppInterface();
141
169
  const toolName = data.data?.tool || data.tool;
142
- const description = getToolDescription(toolName);
143
- // Add tool event as a system message to show in history
170
+ const toolArgs = data.data?.arguments || {};
171
+ // Add tool call as a system message with tool metadata
144
172
  appInterface?.addMessage({
145
173
  id: `tool-${Date.now()}`,
146
174
  type: "system",
147
- content: `🔧 ${toolName.replace(/_/g, " ")} - ${description}`,
175
+ content: "", // Content will be rendered by ToolCallDisplay
176
+ metadata: {
177
+ isTool: true,
178
+ toolName,
179
+ toolArgs,
180
+ isLoading: true,
181
+ },
148
182
  });
149
183
  appInterface?.setStatus({
150
184
  text: "Agent thinking...",
151
185
  type: "loading",
152
186
  });
153
187
  };
154
- const onToolResult = () => {
188
+ const onToolResult = (data) => {
155
189
  if (userCancelled) {
156
190
  return;
157
191
  }
158
- // Tool completion is intentionally silent now.
192
+ const appInterface = getAppInterface();
193
+ const toolName = data.data?.tool || data.tool;
194
+ const result = data.data?.result;
195
+ // Update the last tool message with the result
196
+ appInterface?.updateLastToolMessage?.({
197
+ toolName,
198
+ result,
199
+ isLoading: false,
200
+ });
159
201
  };
160
202
  const onAgentCancelled = () => {
161
203
  if (userCancelled) {
@@ -199,8 +241,117 @@ export const chatInk = new Command()
199
241
  chatContext.toolCount = history.filter((msg) => msg.role === "tool").length;
200
242
  };
201
243
  updateContextFromOrchestrator();
202
- // Define slash commands
244
+ // Define slash commands (alphabetically sorted)
203
245
  const slashCommands = [
246
+ {
247
+ name: "cancel",
248
+ description: "Alias for /stop",
249
+ handler: async () => {
250
+ handleUserCancel();
251
+ return "continue";
252
+ },
253
+ },
254
+ {
255
+ name: "clear",
256
+ description: "Clear the transcript visually",
257
+ handler: async () => "continue",
258
+ },
259
+ {
260
+ name: "bugs",
261
+ description: "List all bugs identified in this session",
262
+ handler: async (args, orchestrator) => {
263
+ const history = orchestrator.getConversationHistory();
264
+ const findings = history
265
+ .filter((msg) => msg.role === "tool" && msg.toolCall?.name === "generate_document")
266
+ .map((msg) => {
267
+ try {
268
+ const res = JSON.parse(msg.content);
269
+ return res.findings || [];
270
+ }
271
+ catch {
272
+ return [];
273
+ }
274
+ })
275
+ .flat();
276
+ const appInterface = getAppInterface();
277
+ if (findings.length === 0) {
278
+ appInterface?.addMessage({
279
+ id: Date.now().toString(),
280
+ type: "system",
281
+ content: "No bugs have been formally reported yet. Use /report to generate a report.",
282
+ });
283
+ }
284
+ else {
285
+ const list = findings
286
+ .map((f, i) => `${i + 1}. [${f.severity?.toUpperCase()}] ${f.title}`)
287
+ .join("\n");
288
+ appInterface?.addMessage({
289
+ id: Date.now().toString(),
290
+ type: "system",
291
+ content: `🐞 Identified Bugs:\n${list}`,
292
+ });
293
+ }
294
+ return "continue";
295
+ },
296
+ },
297
+ {
298
+ name: "compact",
299
+ description: "Compact conversation context to manage token limits",
300
+ handler: async () => {
301
+ const stats = orchestrator.getContextStats();
302
+ const appInterface = getAppInterface();
303
+ if (!stats.needsCompaction) {
304
+ appInterface?.addMessage({
305
+ id: Date.now().toString(),
306
+ type: "system",
307
+ content: `Context: ${stats.totalMessages} messages, ${stats.tokens.toLocaleString()} tokens (${stats.utilizationPercent.toFixed(1)}% of limit), ${stats.conversationTurns} turns. No compaction needed.`,
308
+ });
309
+ return "continue";
310
+ }
311
+ const result = await orchestrator.compactContext();
312
+ appInterface?.addMessage({
313
+ id: Date.now().toString(),
314
+ type: "system",
315
+ content: `✅ Context compacted: ${result.stats.removedCount} messages removed\n` +
316
+ `Before: ${result.stats.tokensBeforeCompaction.toLocaleString()} tokens\n` +
317
+ `After: ${result.stats.tokensAfterCompaction.toLocaleString()} tokens\n` +
318
+ `Strategy: ${result.stats.strategy}` +
319
+ (result.stats.summaryGenerated
320
+ ? "\n📝 LLM summary generated"
321
+ : ""),
322
+ });
323
+ updateContextFromOrchestrator();
324
+ appInterface?.updateContext(chatContext);
325
+ return "continue";
326
+ },
327
+ },
328
+ {
329
+ name: "exit",
330
+ description: "Exit the session (saves progress)",
331
+ handler: async () => "exit",
332
+ },
333
+ {
334
+ name: "goto",
335
+ description: "Direct the agent to a specific URL",
336
+ handler: async (args, orchestrator) => {
337
+ if (args.length === 0) {
338
+ getAppInterface()?.addMessage({
339
+ id: Date.now().toString(),
340
+ type: "system",
341
+ content: "Usage: /goto <url>",
342
+ });
343
+ return "continue";
344
+ }
345
+ const url = args[0];
346
+ getAppInterface()?.addMessage({
347
+ id: Date.now().toString(),
348
+ type: "system",
349
+ content: `🚀 Navigating to ${url}...`,
350
+ });
351
+ void handleUserMessage(`Navigate to ${url} and tell me what you see.`);
352
+ return "continue";
353
+ },
354
+ },
204
355
  {
205
356
  name: "help",
206
357
  description: "Show available slash commands",
@@ -275,6 +426,72 @@ export const chatInk = new Command()
275
426
  return "continue";
276
427
  },
277
428
  },
429
+ {
430
+ name: "report",
431
+ description: "Force the agent to generate a final report",
432
+ handler: async (args, orchestrator) => {
433
+ getAppInterface()?.addMessage({
434
+ id: Date.now().toString(),
435
+ type: "system",
436
+ content: "📑 Generating report based on session history...",
437
+ });
438
+ void handleUserMessage("Please generate a final report of your findings for this session.");
439
+ return "continue";
440
+ },
441
+ },
442
+ {
443
+ name: "scan",
444
+ description: "Instruct the agent to scan the page for interactive elements",
445
+ handler: async () => {
446
+ getAppInterface()?.addMessage({
447
+ id: Date.now().toString(),
448
+ type: "system",
449
+ content: "🔍 Scanning page for interactive elements...",
450
+ });
451
+ void handleUserMessage("Scan the current page and list all interactive elements you find.");
452
+ return "continue";
453
+ },
454
+ },
455
+ {
456
+ name: "screenshot",
457
+ description: "Force the agent to take a screenshot and show the state",
458
+ handler: async () => {
459
+ getAppInterface()?.addMessage({
460
+ id: Date.now().toString(),
461
+ type: "system",
462
+ content: "📸 Capturing fresh screenshot...",
463
+ });
464
+ void handleUserMessage("Please take a screenshot of the current page and tell me what's visible.");
465
+ return "continue";
466
+ },
467
+ },
468
+ {
469
+ name: "where",
470
+ description: "Get the current URL and page title from the agent",
471
+ handler: async () => {
472
+ getAppInterface()?.addMessage({
473
+ id: Date.now().toString(),
474
+ type: "system",
475
+ content: "📍 Checking location...",
476
+ });
477
+ void handleUserMessage("What is the current URL and page title?");
478
+ return "continue";
479
+ },
480
+ },
481
+ {
482
+ name: "reset",
483
+ description: "Reset session and clear all messages",
484
+ handler: async (args, orchestrator) => {
485
+ orchestrator.cancel();
486
+ getAppInterface()?.addMessage({
487
+ id: Date.now().toString(),
488
+ type: "system",
489
+ content: "🧹 Session reset. History cleared.",
490
+ });
491
+ // Note: In a real app we'd want to tell the orchestrator to reset its internal state too
492
+ return "continue";
493
+ },
494
+ },
278
495
  {
279
496
  name: "session",
280
497
  description: "Display session metadata and storage path",
@@ -293,14 +510,6 @@ export const chatInk = new Command()
293
510
  return "continue";
294
511
  },
295
512
  },
296
- {
297
- name: "clear",
298
- description: "Clear the terminal and redraw the header",
299
- handler: async () => {
300
- console.clear();
301
- return "continue";
302
- },
303
- },
304
513
  {
305
514
  name: "stop",
306
515
  description: "Stop the assistant (same as ESC)",
@@ -310,23 +519,28 @@ export const chatInk = new Command()
310
519
  },
311
520
  },
312
521
  {
313
- name: "cancel",
314
- description: "Alias for /stop",
522
+ name: "logout",
523
+ description: "Log out and clear local credentials",
315
524
  handler: async () => {
316
- handleUserCancel();
317
- return "continue";
525
+ try {
526
+ const { clearAuthToken } = await import("../lib/config.js");
527
+ clearAuthToken();
528
+ getAppInterface()?.addMessage({
529
+ id: Date.now().toString(),
530
+ type: "system",
531
+ content: "✅ Logged out. Credentials cleared.",
532
+ });
533
+ }
534
+ catch (e) {
535
+ getAppInterface()?.addMessage({
536
+ id: Date.now().toString(),
537
+ type: "system",
538
+ content: `❌ Failed to logout: ${e?.message || String(e)}`,
539
+ });
540
+ }
541
+ return "exit";
318
542
  },
319
543
  },
320
- {
321
- name: "exit",
322
- description: "Exit the chat session",
323
- handler: async () => "exit",
324
- },
325
- {
326
- name: "quit",
327
- description: "Alias for /exit",
328
- handler: async () => "exit",
329
- },
330
544
  {
331
545
  name: "model",
332
546
  description: "Switch AI model (Pro tier required)",
@@ -350,8 +564,12 @@ export const chatInk = new Command()
350
564
  if (args.length === 0) {
351
565
  const modelsList = availableModels
352
566
  .map((m) => {
353
- const usesInfo = m.usesRemaining !== null ? ` (${m.usesRemaining} free uses left)` : "";
354
- const creditInfo = m.credits > 0 ? ` - ${m.credits} credit${m.credits !== 1 ? 's' : ''}/msg` : "";
567
+ const usesInfo = m.usesRemaining !== null
568
+ ? ` (${m.usesRemaining} free uses left)`
569
+ : "";
570
+ const creditInfo = m.credits > 0
571
+ ? ` - ${m.credits} credit${m.credits !== 1 ? "s" : ""}/msg`
572
+ : "";
355
573
  return ` ${m.id} - ${m.name}${creditInfo}${usesInfo}`;
356
574
  })
357
575
  .join("\n");
@@ -385,7 +603,9 @@ export const chatInk = new Command()
385
603
  appInterface?.addMessage({
386
604
  id: Date.now().toString(),
387
605
  type: "system",
388
- content: model ? `❌ ${model.name} is unavailable` : `❌ Unknown model: ${modelId}`,
606
+ content: model
607
+ ? `❌ ${model.name} is unavailable`
608
+ : `❌ Unknown model: ${modelId}`,
389
609
  });
390
610
  return "continue";
391
611
  }
@@ -397,13 +617,13 @@ export const chatInk = new Command()
397
617
  let statusInfo = "";
398
618
  if (model.credits > 0) {
399
619
  if (isPaidPlan) {
400
- statusInfo = ` (${model.credits} credit${model.credits !== 1 ? 's' : ''}/msg, ${planTier} plan)`;
620
+ statusInfo = ` (${model.credits} credit${model.credits !== 1 ? "s" : ""}/msg, ${planTier} plan)`;
401
621
  }
402
622
  else if (creditBalance > 0) {
403
- statusInfo = ` (${model.credits} credit${model.credits !== 1 ? 's' : ''}/msg, ${creditBalance} credits remaining)`;
623
+ statusInfo = ` (${model.credits} credit${model.credits !== 1 ? "s" : ""}/msg, ${creditBalance} credits remaining)`;
404
624
  }
405
625
  else {
406
- statusInfo = ` (${model.credits} credit${model.credits !== 1 ? 's' : ''}/msg)`;
626
+ statusInfo = ` (${model.credits} credit${model.credits !== 1 ? "s" : ""}/msg)`;
407
627
  }
408
628
  }
409
629
  appInterface?.addMessage({
@@ -423,29 +643,6 @@ export const chatInk = new Command()
423
643
  }
424
644
  },
425
645
  },
426
- {
427
- name: "logout",
428
- description: "Log out and clear local credentials",
429
- handler: async () => {
430
- try {
431
- const { clearAuthToken } = await import("../lib/config.js");
432
- clearAuthToken();
433
- getAppInterface()?.addMessage({
434
- id: Date.now().toString(),
435
- type: "system",
436
- content: "✅ Logged out. Credentials cleared.",
437
- });
438
- }
439
- catch (e) {
440
- getAppInterface()?.addMessage({
441
- id: Date.now().toString(),
442
- type: "system",
443
- content: `❌ Failed to logout: ${e?.message || String(e)}`,
444
- });
445
- }
446
- return "exit";
447
- },
448
- },
449
646
  ];
450
647
  const commandMap = new Map(slashCommands.map((command) => [command.name, command.handler]));
451
648
  const executeSlashCommand = async (cmd, args) => {
@@ -508,7 +705,7 @@ export const chatInk = new Command()
508
705
  };
509
706
  const handleExit = () => {
510
707
  cleanupOrchestratorListeners();
511
- console.log(chalk.green("\n✓ Session saved. Run 'ttt resume' to continue."));
708
+ console.error(chalk.green("\n✓ Session saved. Run 'ttt resume' to continue."));
512
709
  process.exit(0);
513
710
  };
514
711
  // Prepare slash commands for the UI
@@ -519,21 +716,59 @@ export const chatInk = new Command()
519
716
  label: `/${cmd.name}`,
520
717
  }));
521
718
  // Render the INK app
522
- const { waitUntilExit } = render(React.createElement(ChatApp, {
719
+ const { waitUntilExit, clear } = render(React.createElement(ChatApp, {
523
720
  user: userName,
524
721
  context: chatContext,
525
722
  slashCommands: slashCommandsForUI,
526
723
  onUserMessage: handleUserMessage,
527
724
  onSlashCommand: executeSlashCommand,
725
+ onSetTestingMode: async (mode) => {
726
+ orchestrator.setTestingMode(mode);
727
+ updateContextFromOrchestrator();
728
+ getAppInterface()?.updateContext(chatContext);
729
+ // Save mode preference for next session
730
+ await configManager.setLastMode(mode);
731
+ },
528
732
  onExit: handleExit,
529
733
  onCancel: handleUserCancel,
530
- }));
734
+ initialInput: options.initialInput,
735
+ }), {
736
+ stdin: process.stdin,
737
+ stdout: process.stdout,
738
+ debug: false,
739
+ patchConsole: false,
740
+ });
741
+ // Clear and re-render on terminal resize to prevent ghosting
742
+ const handleResize = () => {
743
+ // Clear entire terminal including scrollback
744
+ process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
745
+ // Reset Ink's line counter and force re-render
746
+ clear();
747
+ // Force a re-render by triggering state change
748
+ const appInterface = getAppInterface();
749
+ if (appInterface) {
750
+ appInterface.setStatus({ text: "", type: "info" });
751
+ setTimeout(() => appInterface.clearStatus(), 0);
752
+ }
753
+ };
754
+ process.stdout.on("resize", handleResize);
531
755
  await waitUntilExit();
756
+ process.stdout.off("resize", handleResize);
532
757
  cleanupOrchestratorListeners();
533
758
  }
534
759
  catch (error) {
535
760
  console.error(chalk.red(`Error: ${error.message}`));
536
761
  process.exit(1);
537
762
  }
763
+ }
764
+ export const chatInk = new Command()
765
+ .name("chat")
766
+ .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).")
767
+ .option("--mode <mode>", "Testing mode: ui, api, or code", "ui")
768
+ .option("--base-url <url>", "Base URL for UI testing", process.env.TTT_BASE_URL)
769
+ .option("--api-base-url <url>", "Base URL for API testing", process.env.TTT_API_BASE_URL)
770
+ .option("--session <id>", "Resume existing session by ID")
771
+ .action(async (options) => {
772
+ await runChatInk(options);
538
773
  });
539
774
  //# sourceMappingURL=chat-ink.js.map