@timetotest/cli 0.3.1 → 0.3.3

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 (77) hide show
  1. package/README.md +12 -2
  2. package/dist/bin/ttt.js +0 -6
  3. package/dist/bin/ttt.js.map +1 -1
  4. package/dist/package.json +1 -1
  5. package/dist/src/commands/chat/ChatApp.js +229 -124
  6. package/dist/src/commands/chat/ChatApp.js.map +1 -1
  7. package/dist/src/commands/chat/OnboardingApp.js +13 -6
  8. package/dist/src/commands/chat/OnboardingApp.js.map +1 -1
  9. package/dist/src/commands/chat/components/ChatInput.js +22 -9
  10. package/dist/src/commands/chat/components/ChatInput.js.map +1 -1
  11. package/dist/src/commands/chat/components/ModelPicker.js +62 -0
  12. package/dist/src/commands/chat/components/ModelPicker.js.map +1 -0
  13. package/dist/src/commands/chat/components/TodoPanel.js +71 -0
  14. package/dist/src/commands/chat/components/TodoPanel.js.map +1 -0
  15. package/dist/src/commands/chat/components/ToolCallDisplay.js +45 -28
  16. package/dist/src/commands/chat/components/ToolCallDisplay.js.map +1 -1
  17. package/dist/src/commands/chat/tool-message-matcher.js +20 -0
  18. package/dist/src/commands/chat/tool-message-matcher.js.map +1 -0
  19. package/dist/src/commands/chat-ink.js +200 -341
  20. package/dist/src/commands/chat-ink.js.map +1 -1
  21. package/dist/src/commands/login.js +21 -5
  22. package/dist/src/commands/login.js.map +1 -1
  23. package/dist/src/lib/__tests__/code-mode-integration.test.js +45 -356
  24. package/dist/src/lib/__tests__/code-mode-integration.test.js.map +1 -1
  25. package/dist/src/lib/__tests__/login-auth-flow.test.js +14 -0
  26. package/dist/src/lib/__tests__/login-auth-flow.test.js.map +1 -0
  27. package/dist/src/lib/__tests__/model-picker-window.test.js +20 -0
  28. package/dist/src/lib/__tests__/model-picker-window.test.js.map +1 -0
  29. package/dist/src/lib/__tests__/tool-executor-mode-gating.test.js +46 -0
  30. package/dist/src/lib/__tests__/tool-executor-mode-gating.test.js.map +1 -0
  31. package/dist/src/lib/__tests__/tool-message-matcher.test.js +26 -0
  32. package/dist/src/lib/__tests__/tool-message-matcher.test.js.map +1 -0
  33. package/dist/src/lib/__tests__/ui-browser-integration.test.js +35 -0
  34. package/dist/src/lib/__tests__/ui-browser-integration.test.js.map +1 -0
  35. package/dist/src/lib/agent-orchestrator.js +16 -717
  36. package/dist/src/lib/agent-orchestrator.js.map +1 -1
  37. package/dist/src/lib/backend-loop-client.js +377 -0
  38. package/dist/src/lib/backend-loop-client.js.map +1 -0
  39. package/dist/src/lib/cli-tool-manifest.js +71 -0
  40. package/dist/src/lib/cli-tool-manifest.js.map +1 -0
  41. package/dist/src/lib/config.js +60 -9
  42. package/dist/src/lib/config.js.map +1 -1
  43. package/dist/src/lib/conversation/turns.js +58 -0
  44. package/dist/src/lib/conversation/turns.js.map +1 -0
  45. package/dist/src/lib/events.js +20 -4
  46. package/dist/src/lib/events.js.map +1 -1
  47. package/dist/src/lib/http.js +7 -2
  48. package/dist/src/lib/http.js.map +1 -1
  49. package/dist/src/lib/prompts/templates.js +18 -0
  50. package/dist/src/lib/prompts/templates.js.map +1 -1
  51. package/dist/src/lib/session-manager.js +74 -33
  52. package/dist/src/lib/session-manager.js.map +1 -1
  53. package/dist/src/lib/socket.js +15 -3
  54. package/dist/src/lib/socket.js.map +1 -1
  55. package/dist/src/lib/todo.js +7 -0
  56. package/dist/src/lib/todo.js.map +1 -0
  57. package/dist/src/lib/tool-executor.js +196 -51
  58. package/dist/src/lib/tool-executor.js.map +1 -1
  59. package/dist/src/lib/tui/events.js +10 -9
  60. package/dist/src/lib/tui/events.js.map +1 -1
  61. package/dist/src/lib/tui/ink/components/TimetoTestLogo.js +1 -1
  62. package/dist/src/lib/tui/ink/components/TimetoTestLogo.js.map +1 -1
  63. package/dist/src/lib/utils/json.js +15 -0
  64. package/dist/src/lib/utils/json.js.map +1 -0
  65. package/package.json +1 -1
  66. package/dist/src/commands/report.js +0 -25
  67. package/dist/src/commands/report.js.map +0 -1
  68. package/dist/src/commands/restart.js +0 -17
  69. package/dist/src/commands/restart.js.map +0 -1
  70. package/dist/src/commands/share.js +0 -18
  71. package/dist/src/commands/share.js.map +0 -1
  72. package/dist/src/lib/context-compactor.js +0 -310
  73. package/dist/src/lib/context-compactor.js.map +0 -1
  74. package/dist/src/lib/tool-registry.js +0 -971
  75. package/dist/src/lib/tool-registry.js.map +0 -1
  76. package/dist/src/lib/tool-result-pruner.js +0 -384
  77. package/dist/src/lib/tool-result-pruner.js.map +0 -1
@@ -9,42 +9,58 @@ import { render } from "ink";
9
9
  import { AgentOrchestrator } from "../lib/agent-orchestrator.js";
10
10
  import { resolveApiUrl, getAuthToken, configManager } from "../lib/config.js";
11
11
  import { ChatApp } from "./chat/ChatApp.js";
12
- import { performInteractiveLogin } from "./login.js";
12
+ import { performCliSignup, performInteractiveLogin } from "./login.js";
13
13
  import { resolveTestingMode } from "../lib/testing-mode.js";
14
14
  export async function runChatInk(options) {
15
15
  try {
16
16
  const apiUrl = resolveApiUrl();
17
17
  let token = getAuthToken();
18
+ const isAuthError = (error) => {
19
+ const status = error?.response?.status;
20
+ if (status === 401 || status === 403)
21
+ return true;
22
+ const message = String(error?.message || "");
23
+ return message.includes("status code 401") || message.includes("status code 403");
24
+ };
18
25
  if (!token) {
19
- // Use TUI onboarding instead of direct login
20
- // We need to dynamically import OnboardingApp to avoid top-level side effects if possible
21
26
  const { OnboardingApp } = await import("./chat/OnboardingApp.js");
22
- // Wrap TUI in a promise to wait for user interaction
23
- await new Promise((resolve, reject) => {
27
+ const promptOnboardingAction = () => new Promise((resolve) => {
24
28
  const { unmount } = render(React.createElement(OnboardingApp, {
25
- onLogin: async () => {
29
+ onSignIn: () => {
26
30
  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
- }
31
+ resolve("signin");
32
+ },
33
+ onSignUp: () => {
34
+ unmount();
35
+ resolve("signup");
41
36
  },
42
37
  onExit: () => {
43
38
  unmount();
44
- process.exit(0);
39
+ resolve("exit");
45
40
  },
46
41
  }));
47
42
  });
43
+ while (!token) {
44
+ const action = await promptOnboardingAction();
45
+ if (action === "exit") {
46
+ process.exit(0);
47
+ }
48
+ try {
49
+ if (action === "signup") {
50
+ await performCliSignup();
51
+ }
52
+ else {
53
+ await performInteractiveLogin();
54
+ }
55
+ token = getAuthToken();
56
+ if (!token) {
57
+ throw new Error("Authentication completed but no token was saved.");
58
+ }
59
+ }
60
+ catch (error) {
61
+ console.error(`❌ Authentication failed: ${error?.message || error}\nRetry with Sign In/Sign Up or press Exit.`);
62
+ }
63
+ }
48
64
  }
49
65
  // Fetch user information
50
66
  const { createHttpClient } = await import("../lib/http.js");
@@ -58,24 +74,30 @@ export async function runChatInk(options) {
58
74
  catch (error) {
59
75
  const status = error?.response?.status;
60
76
  if (status === 401 || status === 403) {
61
- // Token expired or invalid - re-authenticate
62
- try {
63
- await performInteractiveLogin();
64
- // Update token after re-authentication
65
- token = getAuthToken();
66
- if (!token) {
67
- console.error(" Re-authentication failed");
68
- process.exit(1);
77
+ let reauthed = false;
78
+ for (let attempt = 1; attempt <= 2; attempt += 1) {
79
+ try {
80
+ await performInteractiveLogin();
81
+ token = getAuthToken();
82
+ if (!token) {
83
+ throw new Error("No token returned after login.");
84
+ }
85
+ http = createHttpClient();
86
+ const retryUserResp = await http.get("/api/v1/auth/me");
87
+ userName =
88
+ retryUserResp.data?.email ||
89
+ retryUserResp.data?.display_name ||
90
+ "Unknown";
91
+ reauthed = true;
92
+ break;
93
+ }
94
+ catch (reauthError) {
95
+ const suffix = attempt < 2 ? " Retrying once..." : " Continuing with limited context.";
96
+ console.error(`❌ Re-authentication failed: ${reauthError?.message || reauthError}.${suffix}`);
69
97
  }
70
- http = createHttpClient();
71
- // Retry fetching user info
72
- const userResp = await http.get("/api/v1/auth/me");
73
- userName =
74
- userResp.data?.email || userResp.data?.display_name || "Unknown";
75
98
  }
76
- catch (reauthError) {
77
- console.error(`❌ Re-authentication failed: ${reauthError?.message || reauthError}`);
78
- process.exit(1);
99
+ if (!reauthed) {
100
+ userName = "Unknown";
79
101
  }
80
102
  }
81
103
  else {
@@ -114,6 +136,15 @@ export async function runChatInk(options) {
114
136
  apiBaseUrl: options.apiBaseUrl,
115
137
  };
116
138
  const orchestrator = new AgentOrchestrator(config, options.session);
139
+ try {
140
+ const preferredModel = await configManager.getPreferredModel();
141
+ if (preferredModel) {
142
+ await orchestrator.setModel(preferredModel);
143
+ }
144
+ }
145
+ catch {
146
+ // Ignore preference load failures; session can proceed with backend default.
147
+ }
117
148
  let userCancelled = false; // Track when the user cancels an operation
118
149
  function getAppInterface() {
119
150
  return globalThis.__chatAppInterface;
@@ -168,6 +199,11 @@ export async function runChatInk(options) {
168
199
  const appInterface = getAppInterface();
169
200
  const toolName = data.data?.tool || data.tool;
170
201
  const toolArgs = data.data?.arguments || {};
202
+ const callId = data.data?.call_id || data.call_id;
203
+ if (toolName === "todo") {
204
+ // Render todo changes in the dedicated panel (avoid transcript spam).
205
+ return;
206
+ }
171
207
  // Add tool call as a system message with tool metadata
172
208
  appInterface?.addMessage({
173
209
  id: `tool-${Date.now()}`,
@@ -176,6 +212,7 @@ export async function runChatInk(options) {
176
212
  metadata: {
177
213
  isTool: true,
178
214
  toolName,
215
+ toolCallId: callId,
179
216
  toolArgs,
180
217
  isLoading: true,
181
218
  },
@@ -192,9 +229,28 @@ export async function runChatInk(options) {
192
229
  const appInterface = getAppInterface();
193
230
  const toolName = data.data?.tool || data.tool;
194
231
  const result = data.data?.result;
232
+ const callId = data.data?.call_id || data.call_id;
233
+ if (toolName === "todo") {
234
+ const todoData = result?.data || result?.todo_list_snapshot || result?.todo_list || {};
235
+ const items = Array.isArray(todoData.todos) ? todoData.todos : [];
236
+ const snapshot = items.length > 0 || typeof todoData.title === "string" || typeof todoData.description === "string"
237
+ ? {
238
+ title: typeof todoData.title === "string" ? todoData.title : undefined,
239
+ description: typeof todoData.description === "string" ? todoData.description : undefined,
240
+ todos: items,
241
+ updated_at: typeof todoData.updated_at === "string"
242
+ ? todoData.updated_at
243
+ : new Date().toISOString(),
244
+ current_id: typeof todoData.current_id === "string" ? todoData.current_id : undefined,
245
+ }
246
+ : null;
247
+ appInterface?.setTodos?.(snapshot);
248
+ return;
249
+ }
195
250
  // Update the last tool message with the result
196
251
  appInterface?.updateLastToolMessage?.({
197
252
  toolName,
253
+ callId,
198
254
  result,
199
255
  isLoading: false,
200
256
  });
@@ -241,117 +297,8 @@ export async function runChatInk(options) {
241
297
  chatContext.toolCount = history.filter((msg) => msg.role === "tool").length;
242
298
  };
243
299
  updateContextFromOrchestrator();
244
- // Define slash commands (alphabetically sorted)
300
+ // Define slash commands (only user-approved set)
245
301
  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
- },
355
302
  {
356
303
  name: "help",
357
304
  description: "Show available slash commands",
@@ -367,149 +314,6 @@ export async function runChatInk(options) {
367
314
  return "continue";
368
315
  },
369
316
  },
370
- {
371
- name: "history",
372
- description: "Show the last few conversation turns",
373
- handler: async () => {
374
- const history = orchestrator.getConversationHistory();
375
- const recent = history.slice(-10);
376
- const historyText = recent
377
- .map((message) => {
378
- const roleLabel = message.role === "user"
379
- ? "You"
380
- : message.role === "assistant"
381
- ? "Agent"
382
- : message.role.toUpperCase();
383
- const content = typeof message.content === "string"
384
- ? message.content
385
- : JSON.stringify(message.content, null, 2);
386
- return `${roleLabel}: ${content}`;
387
- })
388
- .join("\n");
389
- getAppInterface()?.addMessage({
390
- id: Date.now().toString(),
391
- type: "system",
392
- content: `History (${history.length} total):\n${historyText}`,
393
- });
394
- return "continue";
395
- },
396
- },
397
- {
398
- name: "progress",
399
- description: "Show conversation summary and tool usage",
400
- handler: async () => {
401
- const history = orchestrator.getConversationHistory();
402
- const totals = history.reduce((acc, message) => {
403
- acc.total += 1;
404
- acc[message.role] = (acc[message.role] || 0) + 1;
405
- return acc;
406
- }, {
407
- total: 0,
408
- user: 0,
409
- assistant: 0,
410
- tool: 0,
411
- system: 0,
412
- });
413
- let progressText = `Messages: ${totals.total} (you ${totals.user}, agent ${totals.assistant}, tool ${totals.tool})`;
414
- if (totals.tool > 0) {
415
- const toolNames = history
416
- .filter((msg) => msg.role === "tool" && msg.toolCall)
417
- .map((msg) => msg.toolCall.name);
418
- const uniqueTools = Array.from(new Set(toolNames));
419
- progressText += `\nTools used: ${uniqueTools.join(", ")}`;
420
- }
421
- getAppInterface()?.addMessage({
422
- id: Date.now().toString(),
423
- type: "system",
424
- content: progressText,
425
- });
426
- return "continue";
427
- },
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
- },
495
- {
496
- name: "session",
497
- description: "Display session metadata and storage path",
498
- handler: async () => {
499
- const sessionText = [
500
- `ID: ${chatContext.sessionId}`,
501
- `Path: ${chatContext.sessionPath}`,
502
- `Mode: ${chatContext.mode}`,
503
- `Testing mode: ${chatContext.testingMode}`,
504
- ].join("\n");
505
- getAppInterface()?.addMessage({
506
- id: Date.now().toString(),
507
- type: "system",
508
- content: sessionText,
509
- });
510
- return "continue";
511
- },
512
- },
513
317
  {
514
318
  name: "stop",
515
319
  description: "Stop the assistant (same as ESC)",
@@ -518,29 +322,6 @@ export async function runChatInk(options) {
518
322
  return "continue";
519
323
  },
520
324
  },
521
- {
522
- name: "logout",
523
- description: "Log out and clear local credentials",
524
- handler: async () => {
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";
542
- },
543
- },
544
325
  {
545
326
  name: "model",
546
327
  description: "Switch AI model (Pro tier required)",
@@ -562,37 +343,59 @@ export async function runChatInk(options) {
562
343
  }
563
344
  // No args - show available models
564
345
  if (args.length === 0) {
565
- const modelsList = availableModels
566
- .map((m) => {
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
- : "";
573
- return ` ${m.id} - ${m.name}${creditInfo}${usesInfo}`;
574
- })
575
- .join("\n");
576
- // Show appropriate balance/plan info
577
- const planTier = data.planTier || "free";
578
- const isPaidPlan = planTier !== "free";
579
- const creditBalance = data.creditBalance || 0;
580
- let balanceInfo = "";
581
- if (isPaidPlan) {
582
- // Paid plan users - show plan tier
583
- balanceInfo = `Plan: ${planTier.charAt(0).toUpperCase() + planTier.slice(1)}`;
584
- if (creditBalance > 0) {
585
- balanceInfo += ` | Extra credits: ${creditBalance}`;
586
- }
346
+ if (availableModels.length === 0) {
347
+ appInterface?.addMessage({
348
+ id: Date.now().toString(),
349
+ type: "system",
350
+ content: "No models are currently available for your account.",
351
+ });
352
+ return "continue";
587
353
  }
588
- else {
589
- // Free tier - show credits
590
- balanceInfo = `Credits: ${creditBalance}`;
354
+ if (appInterface?.promptModelSelection) {
355
+ const currentModelForPicker = orchestrator.getCurrentModel?.() || data.currentModel;
356
+ const selectedModelId = await appInterface.promptModelSelection({
357
+ models: availableModels,
358
+ currentModel: currentModelForPicker,
359
+ planTier: data.planTier,
360
+ creditBalance: data.creditBalance,
361
+ });
362
+ if (!selectedModelId) {
363
+ appInterface?.addMessage({
364
+ id: Date.now().toString(),
365
+ type: "system",
366
+ content: "Model selection cancelled.",
367
+ });
368
+ return "continue";
369
+ }
370
+ const selectedModel = availableModels.find((m) => m.id === selectedModelId);
371
+ if (!selectedModel) {
372
+ appInterface?.addMessage({
373
+ id: Date.now().toString(),
374
+ type: "system",
375
+ content: `❌ Unknown model: ${selectedModelId}`,
376
+ });
377
+ return "continue";
378
+ }
379
+ await orchestrator.setModel(selectedModelId);
380
+ await configManager.setPreferredModel(selectedModelId);
381
+ const statusInfo = selectedModel.credits > 0
382
+ ? ` (${selectedModel.credits} credit${selectedModel.credits !== 1 ? "s" : ""}/msg)`
383
+ : "";
384
+ appInterface?.addMessage({
385
+ id: Date.now().toString(),
386
+ type: "system",
387
+ content: `✅ Switched to ${selectedModel.name}${statusInfo}`,
388
+ });
389
+ return "continue";
591
390
  }
391
+ // Fallback for non-INK contexts
392
+ const modelsList = availableModels
393
+ .map((m) => ` ${m.id} - ${m.name}`)
394
+ .join("\n");
592
395
  appInterface?.addMessage({
593
396
  id: Date.now().toString(),
594
397
  type: "system",
595
- content: `Available Models:\n${modelsList}\n\n${balanceInfo}\n\nUsage: /model <model-id>`,
398
+ content: `Available Models:\n${modelsList}\n\nUsage: /model <model-id>`,
596
399
  });
597
400
  return "continue";
598
401
  }
@@ -610,6 +413,7 @@ export async function runChatInk(options) {
610
413
  return "continue";
611
414
  }
612
415
  await orchestrator.setModel(modelId);
416
+ await configManager.setPreferredModel(modelId);
613
417
  // Show appropriate success message
614
418
  const planTier = data.planTier || "free";
615
419
  const isPaidPlan = planTier !== "free";
@@ -643,6 +447,34 @@ export async function runChatInk(options) {
643
447
  }
644
448
  },
645
449
  },
450
+ {
451
+ name: "logout",
452
+ description: "Log out and clear local credentials",
453
+ handler: async () => {
454
+ try {
455
+ const { clearAuthToken } = await import("../lib/config.js");
456
+ clearAuthToken();
457
+ getAppInterface()?.addMessage({
458
+ id: Date.now().toString(),
459
+ type: "system",
460
+ content: "✅ Logged out. Credentials cleared.",
461
+ });
462
+ }
463
+ catch (e) {
464
+ getAppInterface()?.addMessage({
465
+ id: Date.now().toString(),
466
+ type: "system",
467
+ content: `❌ Failed to logout: ${e?.message || String(e)}`,
468
+ });
469
+ }
470
+ return "exit";
471
+ },
472
+ },
473
+ {
474
+ name: "exit",
475
+ description: "Exit the session (saves progress)",
476
+ handler: async () => "exit",
477
+ },
646
478
  ];
647
479
  const commandMap = new Map(slashCommands.map((command) => [command.name, command.handler]));
648
480
  const executeSlashCommand = async (cmd, args) => {
@@ -665,7 +497,34 @@ export async function runChatInk(options) {
665
497
  text: "Agent thinking...",
666
498
  type: "loading",
667
499
  });
668
- const response = await orchestrator.chat(message);
500
+ let response = "";
501
+ try {
502
+ response = await orchestrator.chat(message);
503
+ }
504
+ catch (error) {
505
+ if (!isAuthError(error)) {
506
+ throw error;
507
+ }
508
+ appInterface?.setStatus({
509
+ text: "Authentication required. Opening login...",
510
+ type: "info",
511
+ });
512
+ try {
513
+ await performInteractiveLogin();
514
+ token = getAuthToken();
515
+ if (!token) {
516
+ throw new Error("Login did not return a valid token");
517
+ }
518
+ }
519
+ catch (loginError) {
520
+ throw new Error(`Authentication required. Login failed: ${loginError?.message || loginError}. Retry your message or run /logout then sign in again.`);
521
+ }
522
+ appInterface?.setStatus({
523
+ text: "Authenticated. Retrying...",
524
+ type: "loading",
525
+ });
526
+ response = await orchestrator.chat(message);
527
+ }
669
528
  if (userCancelled) {
670
529
  return;
671
530
  }
@@ -705,7 +564,7 @@ export async function runChatInk(options) {
705
564
  };
706
565
  const handleExit = () => {
707
566
  cleanupOrchestratorListeners();
708
- console.error(chalk.green("\n✓ Session saved. Run 'ttt resume' to continue."));
567
+ console.error(chalk.green("\n✓ Session saved. Run 'ttt chat --session <id>' to continue."));
709
568
  process.exit(0);
710
569
  };
711
570
  // Prepare slash commands for the UI