@arbidocs/tui 0.1.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.
package/dist/index.cjs CHANGED
@@ -6,8 +6,8 @@ var core = require('@arbidocs/core');
6
6
  var piTui = require('@mariozechner/pi-tui');
7
7
  var chalk = require('chalk');
8
8
  require('fake-indexeddb/auto');
9
- var prompts = require('@inquirer/prompts');
10
9
  var sdk = require('@arbidocs/sdk');
10
+ var prompts = require('@inquirer/prompts');
11
11
 
12
12
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
13
13
 
@@ -148,6 +148,13 @@ var ChatLog = class extends piTui.Container {
148
148
  this.addChild(new UserMessage(text));
149
149
  this.addChild(new piTui.Spacer(1));
150
150
  }
151
+ /** Add a complete assistant message (non-streaming, e.g. for history restoration). */
152
+ addAssistant(text) {
153
+ const msg = new AssistantMessage();
154
+ msg.setText(text);
155
+ this.addChild(msg);
156
+ this.addChild(new piTui.Spacer(1));
157
+ }
151
158
  /** Add a system/info/error message. */
152
159
  addSystem(message, level = "info") {
153
160
  this.addChild(new SystemMessage(message, level));
@@ -310,9 +317,14 @@ var commands = [
310
317
  { name: "contacts", description: "List contacts (type @name to DM)" },
311
318
  { name: "invite", description: "Invite a contact: /invite user@example.com" },
312
319
  { name: "docs", description: "List documents in current workspace" },
320
+ { name: "upload", description: "Upload a file: /upload <path>" },
321
+ { name: "delete", description: "Delete a document: /delete <doc-id>" },
313
322
  { name: "conversations", description: "List conversations" },
314
323
  { name: "new", description: "Start fresh conversation (clear threading)" },
324
+ { name: "models", description: "List available AI models" },
325
+ { name: "health", description: "Show system health status" },
315
326
  { name: "status", description: "Show auth/workspace/connection status" },
327
+ { name: "logout", description: "Log out and clear credentials" },
316
328
  { name: "exit", description: "Exit TUI" },
317
329
  { name: "quit", description: "Exit TUI" }
318
330
  ];
@@ -334,78 +346,52 @@ function formatHelpText() {
334
346
  " Escape \u2014 Abort streaming"
335
347
  ].join("\n");
336
348
  }
337
- async function interactiveLogin(store) {
338
- const config = store.requireConfig();
339
- const email = await prompts.input({ message: "Email", validate: (v) => v.trim() ? true : "Required" });
340
- const pw = await prompts.password({
341
- message: "Password",
349
+ async function promptSelect(message, choices) {
350
+ return prompts.select({ message, choices });
351
+ }
352
+ async function promptInput(message, required = true) {
353
+ return prompts.input({
354
+ message,
355
+ validate: required ? (v) => v.trim() ? true : "Required" : void 0
356
+ });
357
+ }
358
+ async function promptPassword(message) {
359
+ return prompts.password({
360
+ message,
342
361
  mask: "*",
343
362
  validate: (v) => v ? true : "Required"
344
363
  });
345
- const arbi = sdk.createArbiClient({
346
- baseUrl: config.baseUrl,
347
- deploymentDomain: config.deploymentDomain,
348
- credentials: "omit"
349
- });
350
- await arbi.crypto.initSodium();
351
- const loginResult = await arbi.auth.login({ email, password: pw });
352
- store.saveCredentials({
353
- email,
354
- signingPrivateKeyBase64: arbi.crypto.bytesToBase64(loginResult.signingPrivateKey),
355
- serverSessionKeyBase64: arbi.crypto.bytesToBase64(loginResult.serverSessionKey)
356
- });
364
+ }
365
+ async function promptConfirm(message, defaultValue = true) {
366
+ return prompts.confirm({ message, default: defaultValue });
367
+ }
368
+
369
+ // src/auth.ts
370
+ async function interactiveLogin(store) {
371
+ const config = store.requireConfig();
372
+ const email = await promptInput("Email");
373
+ const pw = await promptPassword("Password");
374
+ const authContext = await core.performPasswordLogin(config, email, pw, store);
357
375
  console.info(`Logged in as ${email}`);
358
- const { data: workspaces3 } = await arbi.fetch.GET("/api/user/workspaces");
359
- const wsList = workspaces3 || [];
360
- let selectedWorkspaceId;
361
- let selectedWorkspaceName;
362
- if (wsList.length === 1) {
363
- selectedWorkspaceId = wsList[0].external_id;
364
- selectedWorkspaceName = wsList[0].name;
365
- store.updateConfig({ selectedWorkspaceId });
366
- console.info(`Workspace: ${wsList[0].name}`);
367
- } else if (wsList.length > 1) {
368
- const choices = wsList.map((ws2) => {
369
- const totalDocs = ws2.shared_document_count + ws2.private_document_count;
370
- return {
371
- name: `${ws2.name} (${totalDocs} docs)`,
372
- value: ws2.external_id,
373
- description: ws2.external_id
374
- };
375
- });
376
- selectedWorkspaceId = await prompts.select({ message: "Select workspace", choices });
377
- const ws = wsList.find((w) => w.external_id === selectedWorkspaceId);
378
- selectedWorkspaceName = ws?.name;
379
- store.updateConfig({ selectedWorkspaceId });
380
- console.info(`Workspace: ${selectedWorkspaceName}`);
381
- } else {
382
- console.info("No workspaces found.");
383
- }
384
- const authContext = await core.resolveAuth(store);
385
- return { authContext, selectedWorkspaceId, selectedWorkspaceName };
376
+ const result = await selectOrCreateWorkspace(authContext, store);
377
+ return { authContext, ...result };
386
378
  }
387
379
  async function interactiveRegister(store) {
388
380
  const config = store.requireConfig();
389
- const email = await prompts.input({ message: "Email", validate: (v) => v.trim() ? true : "Required" });
381
+ const email = await promptInput("Email");
390
382
  const arbi = sdk.createArbiClient({
391
383
  baseUrl: config.baseUrl,
392
384
  deploymentDomain: config.deploymentDomain,
393
385
  credentials: "omit"
394
386
  });
395
387
  await arbi.crypto.initSodium();
396
- const codeMethod = await prompts.select({
397
- message: "Verification method",
398
- choices: [
399
- { name: "I have an invitation code", value: "code" },
400
- { name: "Send me a verification email", value: "email" }
401
- ]
402
- });
388
+ const codeMethod = await promptSelect("Verification method", [
389
+ { name: "I have an invitation code", value: "code" },
390
+ { name: "Send me a verification email", value: "email" }
391
+ ]);
403
392
  let verificationCode;
404
393
  if (codeMethod === "code") {
405
- verificationCode = await prompts.input({
406
- message: "Invitation code",
407
- validate: (v) => v.trim() ? true : "Required"
408
- });
394
+ verificationCode = await promptInput("Invitation code");
409
395
  } else {
410
396
  console.info("Sending verification email...");
411
397
  const verifyResponse = await arbi.fetch.POST("/api/user/verify-email", {
@@ -415,26 +401,15 @@ async function interactiveRegister(store) {
415
401
  throw new Error(`Failed to send verification email: ${JSON.stringify(verifyResponse.error)}`);
416
402
  }
417
403
  console.info("Verification email sent. Check your inbox.");
418
- verificationCode = await prompts.input({
419
- message: "Verification code",
420
- validate: (v) => v.trim() ? true : "Required"
421
- });
404
+ verificationCode = await promptInput("Verification code");
422
405
  }
423
- const pw = await prompts.password({
424
- message: "Password",
425
- mask: "*",
426
- validate: (v) => v ? true : "Required"
427
- });
428
- const confirmPw = await prompts.password({
429
- message: "Confirm password",
430
- mask: "*",
431
- validate: (v) => v ? true : "Required"
432
- });
406
+ const pw = await promptPassword("Password");
407
+ const confirmPw = await promptPassword("Confirm password");
433
408
  if (pw !== confirmPw) {
434
409
  throw new Error("Passwords do not match.");
435
410
  }
436
- const firstName = await prompts.input({ message: "First name (optional)" }) || "User";
437
- const lastName = await prompts.input({ message: "Last name (optional)" }) || "";
411
+ const firstName = await promptInput("First name (optional)", false) || "User";
412
+ const lastName = await promptInput("Last name (optional)", false) || "";
438
413
  await arbi.auth.register({
439
414
  email,
440
415
  password: pw,
@@ -444,11 +419,37 @@ async function interactiveRegister(store) {
444
419
  });
445
420
  console.info(`
446
421
  Registered successfully as ${email}`);
447
- const doLogin = await prompts.confirm({ message: "Log in now?", default: true });
448
- if (doLogin) {
449
- return interactiveLogin(store);
422
+ console.info("Logging in...");
423
+ const authContext = await core.performPasswordLogin(config, email, pw, store);
424
+ console.info(`Logged in as ${email}`);
425
+ const result = await selectOrCreateWorkspace(authContext, store);
426
+ return { authContext, ...result };
427
+ }
428
+ async function selectOrCreateWorkspace(authContext, store) {
429
+ const wsList = await core.workspaces.listWorkspaces(authContext.arbi);
430
+ if (wsList.length === 1) {
431
+ store.updateConfig({ selectedWorkspaceId: wsList[0].external_id });
432
+ console.info(`Workspace: ${wsList[0].name}`);
433
+ return { selectedWorkspaceId: wsList[0].external_id, selectedWorkspaceName: wsList[0].name };
450
434
  }
451
- return null;
435
+ if (wsList.length > 1) {
436
+ const choices = core.formatWorkspaceChoices(wsList);
437
+ const selectedWorkspaceId = await promptSelect("Select workspace", choices);
438
+ const ws = wsList.find((w) => w.external_id === selectedWorkspaceId);
439
+ store.updateConfig({ selectedWorkspaceId });
440
+ console.info(`Workspace: ${ws?.name}`);
441
+ return { selectedWorkspaceId, selectedWorkspaceName: ws?.name };
442
+ }
443
+ console.info("No workspaces found.");
444
+ const shouldCreate = await promptConfirm("Create a new workspace?");
445
+ if (shouldCreate) {
446
+ const name = await promptInput("Workspace name");
447
+ const ws = await core.workspaces.createWorkspace(authContext.arbi, name.trim());
448
+ store.updateConfig({ selectedWorkspaceId: ws.external_id });
449
+ console.info(`Created workspace: ${ws.name}`);
450
+ return { selectedWorkspaceId: ws.external_id, selectedWorkspaceName: ws.name };
451
+ }
452
+ return {};
452
453
  }
453
454
  async function ensureAuthenticated(store) {
454
455
  try {
@@ -460,26 +461,75 @@ async function ensureAuthenticated(store) {
460
461
  };
461
462
  } catch {
462
463
  console.info("Not authenticated.\n");
463
- const action = await prompts.select({
464
- message: "What would you like to do?",
465
- choices: [
466
- { name: "Log in", value: "login" },
467
- { name: "Register a new account", value: "register" },
468
- { name: "Exit", value: "exit" }
469
- ]
470
- });
464
+ }
465
+ while (true) {
466
+ const action = await promptSelect("What would you like to do?", [
467
+ { name: "Log in", value: "login" },
468
+ { name: "Register a new account", value: "register" },
469
+ { name: "Exit", value: "exit" }
470
+ ]);
471
471
  if (action === "exit") {
472
472
  process.exit(0);
473
473
  }
474
- if (action === "register") {
475
- const result = await interactiveRegister(store);
476
- if (!result) {
477
- console.info("Registration complete. Please restart to log in.");
478
- process.exit(0);
474
+ try {
475
+ if (action === "register") {
476
+ return await interactiveRegister(store);
479
477
  }
480
- return result;
478
+ return await interactiveLogin(store);
479
+ } catch (err) {
480
+ console.error(`
481
+ ${core.getErrorMessage(err)}
482
+ `);
481
483
  }
482
- return interactiveLogin(store);
484
+ }
485
+ }
486
+ function requireAuth(tui, message = "Not authenticated. Use /login first.") {
487
+ if (tui.authContext) return true;
488
+ showMessage(tui, message, "error");
489
+ return false;
490
+ }
491
+ function showMessage(tui, message, level) {
492
+ tui.chatLog.addSystem(message, level);
493
+ tui.requestRender();
494
+ }
495
+ async function switchWorkspace(tui, workspaceId) {
496
+ if (!tui.authContext) return null;
497
+ try {
498
+ const ws = await core.selectWorkspaceById(
499
+ tui.authContext.arbi,
500
+ workspaceId,
501
+ tui.authContext.loginResult.serverSessionKey,
502
+ tui.store.requireCredentials().signingPrivateKeyBase64
503
+ );
504
+ tui.state.workspaceId = ws.external_id;
505
+ tui.state.workspaceName = ws.name;
506
+ tui.state.conversationMessageId = null;
507
+ tui.store.clearChatSession();
508
+ tui.store.updateConfig({ selectedWorkspaceId: ws.external_id });
509
+ await tui.refreshWorkspaceContext();
510
+ return ws;
511
+ } catch (err) {
512
+ showMessage(tui, `Failed to switch workspace: ${core.getErrorMessage(err)}`, "error");
513
+ return null;
514
+ }
515
+ }
516
+ async function applyWorkspaceSelection(tui, workspaceId, workspaceName) {
517
+ if (!workspaceId) return;
518
+ const wsCtx = await core.resolveWorkspace(tui.store, workspaceId);
519
+ tui.setWorkspaceContext(wsCtx);
520
+ tui.state.workspaceName = workspaceName ?? null;
521
+ }
522
+ async function runInteractiveFlow(tui, pauseMessage, action, errorPrefix) {
523
+ showMessage(tui, pauseMessage);
524
+ tui.stopTui();
525
+ try {
526
+ const result = await action();
527
+ tui.restartTui();
528
+ return result;
529
+ } catch (err) {
530
+ tui.restartTui();
531
+ showMessage(tui, `${errorPrefix}: ${core.getErrorMessage(err)}`, "error");
532
+ return null;
483
533
  }
484
534
  }
485
535
 
@@ -492,8 +542,7 @@ async function handleCommand(tui, input2) {
492
542
  const args = parts.slice(1);
493
543
  switch (cmd) {
494
544
  case "help":
495
- tui.chatLog.addSystem(formatHelpText());
496
- tui.requestRender();
545
+ showMessage(tui, formatHelpText());
497
546
  return true;
498
547
  case "login":
499
548
  await handleLogin(tui);
@@ -519,284 +568,202 @@ async function handleCommand(tui, input2) {
519
568
  case "docs":
520
569
  await handleListDocs(tui);
521
570
  return true;
571
+ case "upload":
572
+ await handleUpload(tui, args.join(" "));
573
+ return true;
574
+ case "delete":
575
+ await handleDelete(tui, args[0]);
576
+ return true;
522
577
  case "conversations":
523
578
  await handleListConversations(tui);
524
579
  return true;
525
580
  case "new":
526
581
  handleNewConversation(tui);
527
582
  return true;
583
+ case "models":
584
+ await handleModels(tui);
585
+ return true;
586
+ case "health":
587
+ await handleHealth(tui);
588
+ return true;
528
589
  case "status":
529
590
  handleStatus(tui);
530
591
  return true;
592
+ case "logout":
593
+ handleLogout(tui);
594
+ return true;
531
595
  case "exit":
532
596
  case "quit":
533
597
  tui.shutdown();
534
598
  return true;
535
599
  default:
536
- tui.chatLog.addSystem(
537
- `Unknown command: /${cmd}. Type /help for available commands.`,
538
- "warning"
539
- );
540
- tui.requestRender();
600
+ showMessage(tui, `Unknown command: /${cmd}. Type /help for available commands.`, "warning");
541
601
  return true;
542
602
  }
543
603
  }
544
604
  async function handleLogin(tui) {
545
- tui.chatLog.addSystem("Pausing TUI for login...");
546
- tui.requestRender();
547
- tui.stopTui();
548
- try {
549
- const result = await interactiveLogin(tui.store);
605
+ const result = await runInteractiveFlow(
606
+ tui,
607
+ "Pausing TUI for login...",
608
+ () => interactiveLogin(tui.store),
609
+ "Login failed"
610
+ );
611
+ if (result) {
550
612
  tui.setAuthContext(result.authContext);
551
- if (result.selectedWorkspaceId) {
552
- const wsCtx = await core.resolveWorkspace(tui.store, result.selectedWorkspaceId);
553
- tui.setWorkspaceContext(wsCtx);
554
- tui.state.workspaceName = result.selectedWorkspaceName ?? null;
555
- }
556
- tui.restartTui();
557
- tui.chatLog.addSystem("Logged in successfully.");
558
- tui.requestRender();
559
- } catch (err) {
560
- tui.restartTui();
561
- const msg = err instanceof Error ? err.message : String(err);
562
- tui.chatLog.addSystem(`Login failed: ${msg}`, "error");
563
- tui.requestRender();
613
+ await applyWorkspaceSelection(tui, result.selectedWorkspaceId, result.selectedWorkspaceName);
614
+ showMessage(tui, "Logged in successfully.");
564
615
  }
565
616
  }
566
617
  async function handleRegister(tui) {
567
- tui.chatLog.addSystem("Pausing TUI for registration...");
568
- tui.requestRender();
569
- tui.stopTui();
570
- try {
571
- const result = await interactiveRegister(tui.store);
572
- if (result) {
573
- tui.setAuthContext(result.authContext);
574
- if (result.selectedWorkspaceId) {
575
- const wsCtx = await core.resolveWorkspace(tui.store, result.selectedWorkspaceId);
576
- tui.setWorkspaceContext(wsCtx);
577
- tui.state.workspaceName = result.selectedWorkspaceName ?? null;
578
- }
579
- }
580
- tui.restartTui();
581
- tui.chatLog.addSystem(
582
- result ? "Registered and logged in." : "Registered. Use /login to log in."
583
- );
584
- tui.requestRender();
585
- } catch (err) {
586
- tui.restartTui();
587
- const msg = err instanceof Error ? err.message : String(err);
588
- tui.chatLog.addSystem(`Registration failed: ${msg}`, "error");
589
- tui.requestRender();
618
+ const result = await runInteractiveFlow(
619
+ tui,
620
+ "Pausing TUI for registration...",
621
+ () => interactiveRegister(tui.store),
622
+ "Registration failed"
623
+ );
624
+ if (result) {
625
+ tui.setAuthContext(result.authContext);
626
+ await applyWorkspaceSelection(tui, result.selectedWorkspaceId, result.selectedWorkspaceName);
627
+ showMessage(tui, "Registered and logged in.");
590
628
  }
591
629
  }
592
630
  async function handleCreateWorkspace(tui, name) {
593
- if (!tui.authContext) {
594
- tui.chatLog.addSystem("Not authenticated. Use /login first.", "error");
595
- tui.requestRender();
596
- return;
597
- }
631
+ if (!requireAuth(tui)) return;
598
632
  if (!name.trim()) {
599
- tui.chatLog.addSystem("Usage: /create <workspace name>", "warning");
600
- tui.requestRender();
633
+ showMessage(tui, "Usage: /create <workspace name>", "warning");
601
634
  return;
602
635
  }
603
636
  try {
604
- tui.chatLog.addSystem(`Creating workspace "${name}"...`);
605
- tui.requestRender();
637
+ showMessage(tui, `Creating workspace "${name}"...`);
606
638
  const ws = await core.workspaces.createWorkspace(tui.authContext.arbi, name.trim());
607
- const { selectWorkspaceById } = await import('@arbidocs/core');
608
- const selected = await selectWorkspaceById(
609
- tui.authContext.arbi,
610
- ws.external_id,
611
- tui.authContext.loginResult.serverSessionKey,
612
- tui.store.requireCredentials().signingPrivateKeyBase64
613
- );
614
- tui.state.workspaceId = selected.external_id;
615
- tui.state.workspaceName = selected.name;
616
- tui.state.conversationMessageId = null;
617
- tui.store.updateConfig({ selectedWorkspaceId: selected.external_id });
618
- await tui.refreshWorkspaceContext();
619
- tui.chatLog.addSystem(`Created and switched to workspace: ${colors.accentBold(selected.name)}`);
620
- tui.requestRender();
639
+ const selected = await switchWorkspace(tui, ws.external_id);
640
+ if (selected) {
641
+ showMessage(tui, `Created and switched to workspace: ${colors.accentBold(selected.name)}`);
642
+ }
621
643
  } catch (err) {
622
- const msg = err instanceof Error ? err.message : String(err);
623
- tui.chatLog.addSystem(`Failed to create workspace: ${msg}`, "error");
624
- tui.requestRender();
644
+ showMessage(tui, `Failed to create workspace: ${core.getErrorMessage(err)}`, "error");
625
645
  }
626
646
  }
627
647
  async function handleWorkspaceSwitch(tui, workspaceId) {
628
- if (!tui.authContext) {
629
- tui.chatLog.addSystem("Not authenticated. Please restart and log in.", "error");
630
- tui.requestRender();
631
- return;
632
- }
648
+ if (!requireAuth(tui, "Not authenticated. Please restart and log in.")) return;
633
649
  if (!workspaceId) {
634
650
  await handleListWorkspaces(tui);
635
- tui.chatLog.addSystem("Use /workspace <id> to switch.");
636
- tui.requestRender();
651
+ showMessage(tui, "Use /workspace <id> to switch.");
637
652
  return;
638
653
  }
639
- try {
640
- tui.chatLog.addSystem(`Switching to workspace ${workspaceId}...`);
641
- tui.requestRender();
642
- const { selectWorkspaceById } = await import('@arbidocs/core');
643
- const ws = await selectWorkspaceById(
644
- tui.authContext.arbi,
645
- workspaceId,
646
- tui.authContext.loginResult.serverSessionKey,
647
- tui.store.requireCredentials().signingPrivateKeyBase64
648
- );
649
- tui.state.workspaceId = ws.external_id;
650
- tui.state.workspaceName = ws.name;
651
- tui.state.conversationMessageId = null;
652
- tui.store.updateConfig({ selectedWorkspaceId: ws.external_id });
653
- await tui.refreshWorkspaceContext();
654
- tui.chatLog.addSystem(`Switched to workspace: ${colors.accentBold(ws.name)}`);
655
- tui.requestRender();
656
- } catch (err) {
657
- const msg = err instanceof Error ? err.message : String(err);
658
- tui.chatLog.addSystem(`Failed to switch workspace: ${msg}`, "error");
659
- tui.requestRender();
654
+ showMessage(tui, `Switching to workspace ${workspaceId}...`);
655
+ const ws = await switchWorkspace(tui, workspaceId);
656
+ if (ws) {
657
+ showMessage(tui, `Switched to workspace: ${colors.accentBold(ws.name)}`);
660
658
  }
661
659
  }
662
660
  async function handleListWorkspaces(tui) {
663
- if (!tui.authContext) {
664
- tui.chatLog.addSystem("Not authenticated.", "error");
665
- tui.requestRender();
666
- return;
667
- }
661
+ if (!requireAuth(tui, "Not authenticated.")) return;
668
662
  try {
669
663
  const wsList = await core.workspaces.listWorkspaces(tui.authContext.arbi);
670
664
  const lines = wsList.map((ws) => {
671
665
  const current = ws.external_id === tui.state.workspaceId ? colors.accent(" (current)") : "";
672
666
  return ` ${ws.external_id} ${ws.name}${current}`;
673
667
  });
674
- tui.chatLog.addSystem(["Workspaces:", "", ...lines].join("\n"));
675
- tui.requestRender();
668
+ showMessage(tui, ["Workspaces:", "", ...lines].join("\n"));
676
669
  } catch (err) {
677
- const msg = err instanceof Error ? err.message : String(err);
678
- tui.chatLog.addSystem(`Failed to list workspaces: ${msg}`, "error");
679
- tui.requestRender();
670
+ showMessage(tui, `Failed to list workspaces: ${core.getErrorMessage(err)}`, "error");
680
671
  }
681
672
  }
682
673
  async function handleListDocs(tui) {
683
674
  if (!tui.authContext || !tui.state.workspaceId) {
684
- tui.chatLog.addSystem("No workspace selected. Use /workspace <id> first.", "warning");
685
- tui.requestRender();
675
+ showMessage(tui, "No workspace selected. Use /workspace <id> first.", "warning");
686
676
  return;
687
677
  }
688
678
  try {
689
679
  const docs = await core.documents.listDocuments(tui.authContext.arbi, tui.state.workspaceId);
690
680
  if (docs.length === 0) {
691
- tui.chatLog.addSystem("No documents in this workspace.");
681
+ showMessage(tui, "No documents in this workspace.");
692
682
  } else {
693
683
  const lines = docs.map((d) => ` ${d.external_id} ${d.file_name ?? "(unnamed)"}`);
694
- tui.chatLog.addSystem([`Documents (${docs.length}):`, "", ...lines].join("\n"));
684
+ showMessage(tui, [`Documents (${docs.length}):`, "", ...lines].join("\n"));
695
685
  }
696
- tui.requestRender();
697
686
  } catch (err) {
698
- const msg = err instanceof Error ? err.message : String(err);
699
- tui.chatLog.addSystem(`Failed to list documents: ${msg}`, "error");
700
- tui.requestRender();
687
+ showMessage(tui, `Failed to list documents: ${core.getErrorMessage(err)}`, "error");
701
688
  }
702
689
  }
703
690
  async function handleListContacts(tui) {
704
- if (!tui.authContext) {
705
- tui.chatLog.addSystem("Not authenticated. Use /login first.", "error");
706
- tui.requestRender();
707
- return;
708
- }
691
+ if (!requireAuth(tui)) return;
709
692
  try {
710
693
  const contactList = await core.contacts.listContacts(tui.authContext.arbi);
711
- const registered = contactList.filter((c) => c.status === "registered");
712
- const invited = contactList.filter((c) => c.status !== "registered");
713
- if (registered.length === 0 && invited.length === 0) {
714
- tui.chatLog.addSystem("No contacts. Use the web app to add contacts.");
694
+ const { registered, pending } = core.contacts.groupContactsByStatus(contactList);
695
+ if (registered.length === 0 && pending.length === 0) {
696
+ showMessage(tui, "No contacts. Use the web app to add contacts.");
715
697
  } else {
716
698
  const lines = [];
717
699
  if (registered.length > 0) {
718
700
  lines.push(`Contacts (${registered.length}):`, "");
719
701
  for (const c of registered) {
720
- const name = c.user ? `${c.user.given_name} ${c.user.family_name}`.trim() : "";
702
+ const name = core.formatUserName(c.user);
721
703
  const nameStr = name ? colors.muted(` (${name})`) : "";
722
704
  lines.push(` ${c.email}${nameStr}`);
723
705
  }
724
706
  }
725
- if (invited.length > 0) {
707
+ if (pending.length > 0) {
726
708
  if (lines.length > 0) lines.push("");
727
- lines.push(`Pending invitations (${invited.length}):`);
728
- for (const c of invited) {
709
+ lines.push(`Pending invitations (${pending.length}):`);
710
+ for (const c of pending) {
729
711
  lines.push(` ${c.email} \u2014 ${colors.muted(c.status)}`);
730
712
  }
731
713
  }
732
714
  lines.push("", colors.muted("Type @name to start a DM with a registered contact."));
733
- tui.chatLog.addSystem(lines.join("\n"));
715
+ showMessage(tui, lines.join("\n"));
734
716
  }
735
- tui.requestRender();
736
717
  } catch (err) {
737
- const msg = err instanceof Error ? err.message : String(err);
738
- tui.chatLog.addSystem(`Failed to list contacts: ${msg}`, "error");
739
- tui.requestRender();
718
+ showMessage(tui, `Failed to list contacts: ${core.getErrorMessage(err)}`, "error");
740
719
  }
741
720
  }
742
721
  async function handleInviteContact(tui, email) {
743
- if (!tui.authContext) {
744
- tui.chatLog.addSystem("Not authenticated. Use /login first.", "error");
745
- tui.requestRender();
746
- return;
747
- }
722
+ if (!requireAuth(tui)) return;
748
723
  if (!email?.trim()) {
749
- tui.chatLog.addSystem("Usage: /invite user@example.com", "warning");
750
- tui.requestRender();
724
+ showMessage(tui, "Usage: /invite user@example.com", "warning");
751
725
  return;
752
726
  }
753
727
  try {
754
- tui.chatLog.addSystem(`Inviting ${email}...`);
755
- tui.requestRender();
728
+ showMessage(tui, `Inviting ${email}...`);
756
729
  const result = await core.contacts.addContacts(tui.authContext.arbi, [email.trim()]);
757
730
  const contact = result[0];
758
731
  if (contact?.status === "registered") {
759
- tui.chatLog.addSystem(
732
+ showMessage(
733
+ tui,
760
734
  `${colors.success("Added")} ${email} \u2014 already registered. You can DM them with @${email.split("@")[0]}`
761
735
  );
762
736
  } else {
763
- tui.chatLog.addSystem(
737
+ showMessage(
738
+ tui,
764
739
  `${colors.success("Invited")} ${email} \u2014 invitation sent. They'll appear in /contacts once they register.`
765
740
  );
766
741
  }
767
- tui.requestRender();
768
742
  } catch (err) {
769
- const msg = err instanceof Error ? err.message : String(err);
770
- tui.chatLog.addSystem(`Failed to invite contact: ${msg}`, "error");
771
- tui.requestRender();
743
+ showMessage(tui, `Failed to invite contact: ${core.getErrorMessage(err)}`, "error");
772
744
  }
773
745
  }
774
746
  async function handleListConversations(tui) {
775
747
  if (!tui.authContext || !tui.state.workspaceId) {
776
- tui.chatLog.addSystem("No workspace selected. Use /workspace <id> first.", "warning");
777
- tui.requestRender();
748
+ showMessage(tui, "No workspace selected. Use /workspace <id> first.", "warning");
778
749
  return;
779
750
  }
780
751
  try {
781
752
  const convs = await core.conversations.listConversations(tui.authContext.arbi, tui.state.workspaceId);
782
753
  if (convs.length === 0) {
783
- tui.chatLog.addSystem("No conversations in this workspace.");
754
+ showMessage(tui, "No conversations in this workspace.");
784
755
  } else {
785
756
  const lines = convs.map((c) => ` ${c.external_id} ${c.title ?? "(untitled)"}`);
786
- tui.chatLog.addSystem([`Conversations (${convs.length}):`, "", ...lines].join("\n"));
757
+ showMessage(tui, [`Conversations (${convs.length}):`, "", ...lines].join("\n"));
787
758
  }
788
- tui.requestRender();
789
759
  } catch (err) {
790
- const msg = err instanceof Error ? err.message : String(err);
791
- tui.chatLog.addSystem(`Failed to list conversations: ${msg}`, "error");
792
- tui.requestRender();
760
+ showMessage(tui, `Failed to list conversations: ${core.getErrorMessage(err)}`, "error");
793
761
  }
794
762
  }
795
763
  function handleNewConversation(tui) {
796
764
  tui.state.conversationMessageId = null;
797
765
  tui.store.clearChatSession();
798
- tui.chatLog.addSystem("Started new conversation.");
799
- tui.requestRender();
766
+ showMessage(tui, "Started new conversation.");
800
767
  }
801
768
  function handleStatus(tui) {
802
769
  const { state } = tui;
@@ -808,8 +775,112 @@ function handleStatus(tui) {
808
775
  `Status: ${state.activityStatus}`,
809
776
  `WebSocket: ${tui.wsConnected ? colors.success("connected") : colors.muted("disconnected")}`
810
777
  ];
811
- tui.chatLog.addSystem(lines.join("\n"));
812
- tui.requestRender();
778
+ showMessage(tui, lines.join("\n"));
779
+ }
780
+ async function handleUpload(tui, filePath) {
781
+ const wsCtx = tui.wsContext;
782
+ if (!wsCtx) {
783
+ showMessage(tui, "No workspace selected. Use /workspace <id> first.", "warning");
784
+ return;
785
+ }
786
+ if (!filePath.trim()) {
787
+ showMessage(tui, "Usage: /upload <file-path>", "warning");
788
+ return;
789
+ }
790
+ try {
791
+ showMessage(tui, `Uploading ${filePath.trim()}...`);
792
+ const result = await core.documents.uploadLocalFile(
793
+ {
794
+ baseUrl: wsCtx.config.baseUrl,
795
+ accessToken: wsCtx.accessToken,
796
+ workspaceKeyHeader: wsCtx.workspaceKeyHeader
797
+ },
798
+ wsCtx.workspaceId,
799
+ filePath.trim()
800
+ );
801
+ const ids = result.doc_ext_ids.join(", ");
802
+ showMessage(tui, `${colors.success("Uploaded")} ${result.fileName} \u2014 doc ID(s): ${ids}`);
803
+ if (result.duplicates && result.duplicates.length > 0) {
804
+ showMessage(tui, `Duplicates detected: ${result.duplicates.join(", ")}`, "warning");
805
+ }
806
+ } catch (err) {
807
+ showMessage(tui, `Failed to upload: ${core.getErrorMessage(err)}`, "error");
808
+ }
809
+ }
810
+ async function handleDelete(tui, docId) {
811
+ if (!requireAuth(tui)) return;
812
+ if (!docId?.trim()) {
813
+ showMessage(tui, "Usage: /delete <doc-id> (use /docs to list document IDs)", "warning");
814
+ return;
815
+ }
816
+ try {
817
+ showMessage(tui, `Deleting document ${docId}...`);
818
+ await core.documents.deleteDocuments(tui.authContext.arbi, [docId.trim()]);
819
+ showMessage(tui, `${colors.success("Deleted")} document ${docId}`);
820
+ } catch (err) {
821
+ showMessage(tui, `Failed to delete document: ${core.getErrorMessage(err)}`, "error");
822
+ }
823
+ }
824
+ function handleLogout(tui) {
825
+ tui.store.deleteCredentials();
826
+ tui.store.updateConfig({ selectedWorkspaceId: void 0 });
827
+ tui.store.clearChatSession();
828
+ tui.authContext = null;
829
+ tui.state.isAuthenticated = false;
830
+ tui.state.workspaceId = null;
831
+ tui.state.workspaceName = null;
832
+ tui.state.conversationMessageId = null;
833
+ showMessage(tui, "Logged out. Use /login to authenticate again.");
834
+ }
835
+ async function handleHealth(tui) {
836
+ if (!requireAuth(tui)) return;
837
+ try {
838
+ const data = await core.health.getHealth(tui.authContext.arbi);
839
+ const lines = ["System Health:", ""];
840
+ const status = data.status;
841
+ if (status) {
842
+ const statusColor = status === "healthy" ? colors.success(status) : colors.warning(status);
843
+ lines.push(` Status: ${statusColor}`);
844
+ }
845
+ const services = data.services;
846
+ if (services) {
847
+ lines.push("", " Services:");
848
+ for (const [name, info] of Object.entries(services)) {
849
+ const svc = info;
850
+ const svcStatus = svc.status;
851
+ const statusStr = svcStatus ? svcStatus === "healthy" ? colors.success(svcStatus) : colors.error(svcStatus) : colors.muted("unknown");
852
+ lines.push(` ${name}: ${statusStr}`);
853
+ }
854
+ }
855
+ showMessage(tui, lines.join("\n"));
856
+ } catch (err) {
857
+ showMessage(tui, `Failed to fetch health status: ${core.getErrorMessage(err)}`, "error");
858
+ }
859
+ }
860
+ async function handleModels(tui) {
861
+ if (!requireAuth(tui)) return;
862
+ try {
863
+ const data = await core.health.getHealthModels(tui.authContext.arbi);
864
+ const models = Array.isArray(data) ? data : data.data ?? [];
865
+ if (models.length === 0) {
866
+ showMessage(tui, "No models available.");
867
+ return;
868
+ }
869
+ const lines = [`Models (${models.length}):`, ""];
870
+ for (const m of models) {
871
+ const model = m;
872
+ const name = model.model_name ?? model.name ?? "unknown";
873
+ const provider = model.provider ?? "";
874
+ const apiType = model.api_type ?? "";
875
+ const parts = [colors.accent(name)];
876
+ if (provider) parts.push(`provider: ${provider}`);
877
+ if (apiType) parts.push(`api: ${apiType}`);
878
+ lines.push(` ${parts.join(" ")}`);
879
+ }
880
+ showMessage(tui, lines.join("\n"));
881
+ } catch (err) {
882
+ showMessage(tui, `Failed to fetch models: ${core.getErrorMessage(err)}`, "error");
883
+ }
813
884
  }
814
885
  async function streamResponse(tui, response) {
815
886
  tui.chatLog.startAssistant();
@@ -848,8 +919,7 @@ async function streamResponse(tui, response) {
848
919
  } catch (err) {
849
920
  tui.chatLog.finalizeAssistant();
850
921
  tui.state.activityStatus = "error";
851
- const msg = err instanceof Error ? err.message : String(err);
852
- tui.chatLog.addSystem(`Streaming failed: ${msg}`, "error");
922
+ tui.chatLog.addSystem(`Streaming failed: ${core.getErrorMessage(err)}`, "error");
853
923
  tui.requestRender();
854
924
  throw err;
855
925
  }
@@ -864,8 +934,7 @@ function deriveEncryptionKeypair(signingPrivateKeyBase64) {
864
934
  }
865
935
  async function resolveRecipient(tui, query) {
866
936
  if (!tui.authContext) {
867
- tui.chatLog.addSystem("Not authenticated. Use /login first.", "error");
868
- tui.requestRender();
937
+ showMessage(tui, "Not authenticated. Use /login first.", "error");
869
938
  return null;
870
939
  }
871
940
  const queryLower = query.toLowerCase();
@@ -880,21 +949,17 @@ async function resolveRecipient(tui, query) {
880
949
  return emailPrefix === queryLower || givenName === queryLower;
881
950
  });
882
951
  if (matches.length === 0) {
883
- tui.chatLog.addSystem(
952
+ showMessage(
953
+ tui,
884
954
  `No registered contact matching "${query}". Use /contacts to see available contacts.`,
885
955
  "warning"
886
956
  );
887
- tui.requestRender();
888
957
  return null;
889
958
  }
890
959
  if (matches.length > 1) {
891
960
  const names = matches.map((c) => ` ${c.email} (${c.user?.given_name ?? ""} ${c.user?.family_name ?? ""})`).join("\n");
892
- tui.chatLog.addSystem(
893
- `Ambiguous match for "${query}". Be more specific:
894
- ${names}`,
895
- "warning"
896
- );
897
- tui.requestRender();
961
+ showMessage(tui, `Ambiguous match for "${query}". Be more specific:
962
+ ${names}`, "warning");
898
963
  return null;
899
964
  }
900
965
  const contact = matches[0];
@@ -907,17 +972,14 @@ ${names}`,
907
972
  family_name: user.family_name
908
973
  };
909
974
  } catch (err) {
910
- const msg = err instanceof Error ? err.message : String(err);
911
- tui.chatLog.addSystem(`Failed to resolve contact: ${msg}`, "error");
912
- tui.requestRender();
975
+ showMessage(tui, `Failed to resolve contact: ${core.getErrorMessage(err)}`, "error");
913
976
  return null;
914
977
  }
915
978
  }
916
979
  async function handleChannelSwitch(tui, input2) {
917
980
  const query = input2.slice(1).trim();
918
981
  if (!query) {
919
- tui.chatLog.addSystem("Usage: @username to switch to DM, @arbi to switch back.", "warning");
920
- tui.requestRender();
982
+ showMessage(tui, "Usage: @username to switch to DM, @arbi to switch back.", "warning");
921
983
  return;
922
984
  }
923
985
  if (query.toLowerCase() === "arbi") {
@@ -930,30 +992,24 @@ async function handleChannelSwitch(tui, input2) {
930
992
  }
931
993
  async function switchToDmChannel(tui, user) {
932
994
  if (!tui.authContext || !tui.encryptionKeys) {
933
- tui.chatLog.addSystem("Not authenticated or encryption not initialized.", "error");
934
- tui.requestRender();
995
+ showMessage(tui, "Not authenticated or encryption not initialized.", "error");
935
996
  return;
936
997
  }
937
998
  tui.currentDmChannel = { user };
938
999
  tui.updateHeader();
939
1000
  tui.chatLog.clearMessages();
940
- tui.chatLog.addSystem(
941
- `Switched to DM with ${colors.accent(user.email)}. Type @arbi to switch back.`
942
- );
943
- tui.requestRender();
1001
+ showMessage(tui, `Switched to DM with ${colors.accent(user.email)}. Type @arbi to switch back.`);
944
1002
  await loadDmHistory(tui, user);
945
1003
  }
946
1004
  function switchToArbi(tui) {
947
1005
  if (!tui.currentDmChannel) {
948
- tui.chatLog.addSystem("Already in AI chat mode.", "info");
949
- tui.requestRender();
1006
+ showMessage(tui, "Already in AI chat mode.", "info");
950
1007
  return;
951
1008
  }
952
1009
  tui.currentDmChannel = null;
953
1010
  tui.updateHeader();
954
1011
  tui.chatLog.clearMessages();
955
- tui.chatLog.addSystem("Switched back to AI chat.");
956
- tui.requestRender();
1012
+ showMessage(tui, "Switched back to AI chat.");
957
1013
  }
958
1014
  async function loadDmHistory(tui, user) {
959
1015
  if (!tui.authContext || !tui.encryptionKeys) return;
@@ -963,9 +1019,7 @@ async function loadDmHistory(tui, user) {
963
1019
  const conversation = allDms.filter(
964
1020
  (msg) => msg.type === "user_message" && (msg.sender.external_id === user.external_id && msg.recipient.external_id === myExtId || msg.sender.external_id === myExtId && msg.recipient.external_id === user.external_id)
965
1021
  );
966
- conversation.sort(
967
- (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
968
- );
1022
+ conversation.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
969
1023
  const unreadIds = conversation.filter((msg) => !msg.read && msg.sender.external_id === user.external_id).map((msg) => msg.external_id);
970
1024
  if (unreadIds.length > 0) {
971
1025
  core.dm.markRead(tui.authContext.arbi, unreadIds).catch(() => {
@@ -980,15 +1034,12 @@ async function loadDmHistory(tui, user) {
980
1034
  }
981
1035
  tui.requestRender();
982
1036
  } catch (err) {
983
- const errMsg = err instanceof Error ? err.message : String(err);
984
- tui.chatLog.addSystem(`Failed to load DM history: ${errMsg}`, "error");
985
- tui.requestRender();
1037
+ showMessage(tui, `Failed to load DM history: ${core.getErrorMessage(err)}`, "error");
986
1038
  }
987
1039
  }
988
1040
  async function sendEncryptedDm(tui, plaintext) {
989
1041
  if (!tui.authContext || !tui.encryptionKeys || !tui.currentDmChannel) {
990
- tui.chatLog.addSystem("Cannot send DM \u2014 not in a DM channel.", "error");
991
- tui.requestRender();
1042
+ showMessage(tui, "Cannot send DM \u2014 not in a DM channel.", "error");
992
1043
  return;
993
1044
  }
994
1045
  const recipient = tui.currentDmChannel.user;
@@ -1004,9 +1055,7 @@ async function sendEncryptedDm(tui, plaintext) {
1004
1055
  displayDmMessage(tui, plaintext, true, recipient.email);
1005
1056
  tui.requestRender();
1006
1057
  } catch (err) {
1007
- const msg = err instanceof Error ? err.message : String(err);
1008
- tui.chatLog.addSystem(`Failed to send DM: ${msg}`, "error");
1009
- tui.requestRender();
1058
+ showMessage(tui, `Failed to send DM: ${core.getErrorMessage(err)}`, "error");
1010
1059
  }
1011
1060
  }
1012
1061
  async function handleIncomingDm(tui, msg) {
@@ -1097,12 +1146,21 @@ function handleBatchComplete(msg, toasts) {
1097
1146
  async function connectTuiWebSocket(options) {
1098
1147
  const { baseUrl, accessToken, toasts, tui } = options;
1099
1148
  try {
1100
- const connection = await core.connectWebSocket({
1149
+ const connection = await core.connectWithReconnect({
1101
1150
  baseUrl,
1102
1151
  accessToken,
1103
1152
  onMessage: (msg) => handleMessage(msg, toasts, tui ?? null),
1104
1153
  onClose: (_code, _reason) => {
1105
1154
  toasts.show("WebSocket disconnected", "warning", DURATION_DEFAULT);
1155
+ },
1156
+ onReconnecting: (attempt, maxRetries) => {
1157
+ toasts.show(`Reconnecting... (${attempt}/${maxRetries})`, "warning", DURATION_DEFAULT);
1158
+ },
1159
+ onReconnected: () => {
1160
+ toasts.show("WebSocket reconnected", "success", DURATION_DEFAULT);
1161
+ },
1162
+ onReconnectFailed: () => {
1163
+ toasts.show("WebSocket reconnection failed", "error", DURATION_LONG);
1106
1164
  }
1107
1165
  });
1108
1166
  return connection;
@@ -1150,14 +1208,7 @@ var ArbiTui = class {
1150
1208
  this.chatLog = new ChatLog();
1151
1209
  this.toastContainer = new ToastContainer();
1152
1210
  this.toastContainer.setRenderCallback(() => this.tui.requestRender());
1153
- this.editor = new ArbiEditor(this.tui);
1154
- this.editor.setAutocompleteProvider(new piTui.CombinedAutocompleteProvider(commands));
1155
- this.editor.onSubmit = (text) => this.handleSubmit(text);
1156
- this.editor.onEscape = () => this.handleAbort();
1157
- this.editor.onCtrlC = () => this.shutdown();
1158
- this.editor.onCtrlD = () => this.shutdown();
1159
- this.editor.onCtrlW = () => this.handleSubmit("/workspaces");
1160
- this.editor.onCtrlN = () => this.handleSubmit("/new");
1211
+ this.editor = this.createEditor();
1161
1212
  this.tui.addChild(this.header);
1162
1213
  this.tui.addChild(new piTui.Spacer(1));
1163
1214
  this.tui.addChild(this.toastContainer);
@@ -1198,20 +1249,25 @@ var ArbiTui = class {
1198
1249
  this.tui.addChild(new piTui.Spacer(1));
1199
1250
  this.tui.addChild(this.toastContainer);
1200
1251
  this.tui.addChild(this.chatLog);
1201
- this.editor = new ArbiEditor(this.tui);
1202
- this.editor.setAutocompleteProvider(new piTui.CombinedAutocompleteProvider(commands));
1203
- this.editor.onSubmit = (text) => this.handleSubmit(text);
1204
- this.editor.onEscape = () => this.handleAbort();
1205
- this.editor.onCtrlC = () => this.shutdown();
1206
- this.editor.onCtrlD = () => this.shutdown();
1207
- this.editor.onCtrlW = () => this.handleSubmit("/workspaces");
1208
- this.editor.onCtrlN = () => this.handleSubmit("/new");
1252
+ this.editor = this.createEditor();
1209
1253
  this.tui.addChild(this.editor);
1210
1254
  this.tui.setFocus(this.editor);
1211
1255
  this.tui.start();
1212
1256
  this.updateHeader();
1213
1257
  this.tui.requestRender();
1214
1258
  }
1259
+ /** Create and wire a new editor instance. */
1260
+ createEditor() {
1261
+ const editor = new ArbiEditor(this.tui);
1262
+ editor.setAutocompleteProvider(new piTui.CombinedAutocompleteProvider(commands));
1263
+ editor.onSubmit = (text) => this.handleSubmit(text);
1264
+ editor.onEscape = () => this.handleAbort();
1265
+ editor.onCtrlC = () => this.shutdown();
1266
+ editor.onCtrlD = () => this.shutdown();
1267
+ editor.onCtrlW = () => this.handleSubmit("/workspaces");
1268
+ editor.onCtrlN = () => this.handleSubmit("/new");
1269
+ return editor;
1270
+ }
1215
1271
  /** Request a render update. */
1216
1272
  requestRender() {
1217
1273
  this.tui.requestRender();
@@ -1241,6 +1297,10 @@ var ArbiTui = class {
1241
1297
  this.workspaceContext = ctx;
1242
1298
  this.updateHeader();
1243
1299
  }
1300
+ /** Public accessor for workspace context (used by command handlers). */
1301
+ get wsContext() {
1302
+ return this.workspaceContext;
1303
+ }
1244
1304
  /** Whether the WebSocket is currently connected. */
1245
1305
  get wsConnected() {
1246
1306
  return this.wsConnection !== null;
@@ -1318,12 +1378,18 @@ var ArbiTui = class {
1318
1378
  const result = await streamResponse(this, response);
1319
1379
  if (result.assistantMessageExtId) {
1320
1380
  this.state.conversationMessageId = result.assistantMessageExtId;
1321
- this.store.updateChatSession({ lastMessageExtId: result.assistantMessageExtId });
1381
+ const sessionUpdate = {
1382
+ lastMessageExtId: result.assistantMessageExtId
1383
+ };
1384
+ const conversationExtId = result.userMessage?.conversation_ext_id ?? result.metadata?.conversation_ext_id;
1385
+ if (conversationExtId) {
1386
+ sessionUpdate.conversationExtId = conversationExtId;
1387
+ }
1388
+ this.store.updateChatSession(sessionUpdate);
1322
1389
  }
1323
1390
  } catch (err) {
1324
1391
  this.state.activityStatus = "error";
1325
- const msg = err instanceof Error ? err.message : String(err);
1326
- this.chatLog.addSystem(`Error: ${msg}`, "error");
1392
+ this.chatLog.addSystem(`Error: ${core.getErrorMessage(err)}`, "error");
1327
1393
  }
1328
1394
  this.state.activityStatus = "idle";
1329
1395
  this.updateHeader();
@@ -1388,8 +1454,7 @@ program.name("arbi-tui").description("Interactive terminal UI for ARBI \u2014 ch
1388
1454
  }
1389
1455
  }
1390
1456
  } catch (err) {
1391
- const msg = err instanceof Error ? err.message : String(err);
1392
- tui.chatLog.addSystem(`Failed to load workspace: ${msg}`, "warning");
1457
+ tui.chatLog.addSystem(`Failed to load workspace: ${core.getErrorMessage(err)}`, "warning");
1393
1458
  tui.chatLog.addSystem("Use /workspaces to list and /workspace <id> to select.");
1394
1459
  }
1395
1460
  } else {
@@ -1401,6 +1466,29 @@ program.name("arbi-tui").description("Interactive terminal UI for ARBI \u2014 ch
1401
1466
  if (session.lastMessageExtId) {
1402
1467
  tui.state.conversationMessageId = session.lastMessageExtId;
1403
1468
  }
1469
+ if (session.conversationExtId && session.lastMessageExtId && tui.authContext) {
1470
+ try {
1471
+ const threads = await core.conversations.getConversationThreads(
1472
+ tui.authContext.arbi,
1473
+ session.conversationExtId
1474
+ );
1475
+ const threadList = threads.threads ?? [];
1476
+ const thread = threadList.find(
1477
+ (t) => t.leaf_message_ext_id === session.lastMessageExtId
1478
+ );
1479
+ if (thread?.history && thread.history.length > 0) {
1480
+ for (const msg of thread.history) {
1481
+ if (msg.role === "user") {
1482
+ tui.chatLog.addUser(msg.content);
1483
+ } else if (msg.role === "assistant") {
1484
+ tui.chatLog.addAssistant(msg.content);
1485
+ }
1486
+ }
1487
+ tui.chatLog.addSystem("--- restored from previous session ---");
1488
+ }
1489
+ } catch {
1490
+ }
1491
+ }
1404
1492
  tui.chatLog.addSystem(
1405
1493
  "Type a question to chat with the ARBI assistant. Use /help for commands."
1406
1494
  );