@arbidocs/tui 0.1.2 → 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 +427 -303
- package/dist/index.cjs.map +1 -1
- package/package.json +3 -3
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));
|
|
@@ -308,10 +315,16 @@ var commands = [
|
|
|
308
315
|
{ name: "workspace", description: "Switch workspace: /workspace <id>" },
|
|
309
316
|
{ name: "workspaces", description: "List all workspaces" },
|
|
310
317
|
{ name: "contacts", description: "List contacts (type @name to DM)" },
|
|
318
|
+
{ name: "invite", description: "Invite a contact: /invite user@example.com" },
|
|
311
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>" },
|
|
312
322
|
{ name: "conversations", description: "List conversations" },
|
|
313
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" },
|
|
314
326
|
{ name: "status", description: "Show auth/workspace/connection status" },
|
|
327
|
+
{ name: "logout", description: "Log out and clear credentials" },
|
|
315
328
|
{ name: "exit", description: "Exit TUI" },
|
|
316
329
|
{ name: "quit", description: "Exit TUI" }
|
|
317
330
|
];
|
|
@@ -333,78 +346,52 @@ function formatHelpText() {
|
|
|
333
346
|
" Escape \u2014 Abort streaming"
|
|
334
347
|
].join("\n");
|
|
335
348
|
}
|
|
336
|
-
async function
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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,
|
|
341
361
|
mask: "*",
|
|
342
362
|
validate: (v) => v ? true : "Required"
|
|
343
363
|
});
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
store.
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
});
|
|
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);
|
|
356
375
|
console.info(`Logged in as ${email}`);
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
let selectedWorkspaceId;
|
|
360
|
-
let selectedWorkspaceName;
|
|
361
|
-
if (wsList.length === 1) {
|
|
362
|
-
selectedWorkspaceId = wsList[0].external_id;
|
|
363
|
-
selectedWorkspaceName = wsList[0].name;
|
|
364
|
-
store.updateConfig({ selectedWorkspaceId });
|
|
365
|
-
console.info(`Workspace: ${wsList[0].name}`);
|
|
366
|
-
} else if (wsList.length > 1) {
|
|
367
|
-
const choices = wsList.map((ws2) => {
|
|
368
|
-
const totalDocs = ws2.shared_document_count + ws2.private_document_count;
|
|
369
|
-
return {
|
|
370
|
-
name: `${ws2.name} (${totalDocs} docs)`,
|
|
371
|
-
value: ws2.external_id,
|
|
372
|
-
description: ws2.external_id
|
|
373
|
-
};
|
|
374
|
-
});
|
|
375
|
-
selectedWorkspaceId = await prompts.select({ message: "Select workspace", choices });
|
|
376
|
-
const ws = wsList.find((w) => w.external_id === selectedWorkspaceId);
|
|
377
|
-
selectedWorkspaceName = ws?.name;
|
|
378
|
-
store.updateConfig({ selectedWorkspaceId });
|
|
379
|
-
console.info(`Workspace: ${selectedWorkspaceName}`);
|
|
380
|
-
} else {
|
|
381
|
-
console.info("No workspaces found.");
|
|
382
|
-
}
|
|
383
|
-
const authContext = await core.resolveAuth(store);
|
|
384
|
-
return { authContext, selectedWorkspaceId, selectedWorkspaceName };
|
|
376
|
+
const result = await selectOrCreateWorkspace(authContext, store);
|
|
377
|
+
return { authContext, ...result };
|
|
385
378
|
}
|
|
386
379
|
async function interactiveRegister(store) {
|
|
387
380
|
const config = store.requireConfig();
|
|
388
|
-
const email = await
|
|
381
|
+
const email = await promptInput("Email");
|
|
389
382
|
const arbi = sdk.createArbiClient({
|
|
390
383
|
baseUrl: config.baseUrl,
|
|
391
384
|
deploymentDomain: config.deploymentDomain,
|
|
392
385
|
credentials: "omit"
|
|
393
386
|
});
|
|
394
387
|
await arbi.crypto.initSodium();
|
|
395
|
-
const codeMethod = await
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
{ name: "Send me a verification email", value: "email" }
|
|
400
|
-
]
|
|
401
|
-
});
|
|
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
|
+
]);
|
|
402
392
|
let verificationCode;
|
|
403
393
|
if (codeMethod === "code") {
|
|
404
|
-
verificationCode = await
|
|
405
|
-
message: "Invitation code",
|
|
406
|
-
validate: (v) => v.trim() ? true : "Required"
|
|
407
|
-
});
|
|
394
|
+
verificationCode = await promptInput("Invitation code");
|
|
408
395
|
} else {
|
|
409
396
|
console.info("Sending verification email...");
|
|
410
397
|
const verifyResponse = await arbi.fetch.POST("/api/user/verify-email", {
|
|
@@ -414,26 +401,15 @@ async function interactiveRegister(store) {
|
|
|
414
401
|
throw new Error(`Failed to send verification email: ${JSON.stringify(verifyResponse.error)}`);
|
|
415
402
|
}
|
|
416
403
|
console.info("Verification email sent. Check your inbox.");
|
|
417
|
-
verificationCode = await
|
|
418
|
-
message: "Verification code",
|
|
419
|
-
validate: (v) => v.trim() ? true : "Required"
|
|
420
|
-
});
|
|
404
|
+
verificationCode = await promptInput("Verification code");
|
|
421
405
|
}
|
|
422
|
-
const pw = await
|
|
423
|
-
|
|
424
|
-
mask: "*",
|
|
425
|
-
validate: (v) => v ? true : "Required"
|
|
426
|
-
});
|
|
427
|
-
const confirmPw = await prompts.password({
|
|
428
|
-
message: "Confirm password",
|
|
429
|
-
mask: "*",
|
|
430
|
-
validate: (v) => v ? true : "Required"
|
|
431
|
-
});
|
|
406
|
+
const pw = await promptPassword("Password");
|
|
407
|
+
const confirmPw = await promptPassword("Confirm password");
|
|
432
408
|
if (pw !== confirmPw) {
|
|
433
409
|
throw new Error("Passwords do not match.");
|
|
434
410
|
}
|
|
435
|
-
const firstName = await
|
|
436
|
-
const lastName = await
|
|
411
|
+
const firstName = await promptInput("First name (optional)", false) || "User";
|
|
412
|
+
const lastName = await promptInput("Last name (optional)", false) || "";
|
|
437
413
|
await arbi.auth.register({
|
|
438
414
|
email,
|
|
439
415
|
password: pw,
|
|
@@ -443,11 +419,37 @@ async function interactiveRegister(store) {
|
|
|
443
419
|
});
|
|
444
420
|
console.info(`
|
|
445
421
|
Registered successfully as ${email}`);
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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 };
|
|
449
434
|
}
|
|
450
|
-
|
|
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 {};
|
|
451
453
|
}
|
|
452
454
|
async function ensureAuthenticated(store) {
|
|
453
455
|
try {
|
|
@@ -459,26 +461,75 @@ async function ensureAuthenticated(store) {
|
|
|
459
461
|
};
|
|
460
462
|
} catch {
|
|
461
463
|
console.info("Not authenticated.\n");
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
});
|
|
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
|
+
]);
|
|
470
471
|
if (action === "exit") {
|
|
471
472
|
process.exit(0);
|
|
472
473
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
console.info("Registration complete. Please restart to log in.");
|
|
477
|
-
process.exit(0);
|
|
474
|
+
try {
|
|
475
|
+
if (action === "register") {
|
|
476
|
+
return await interactiveRegister(store);
|
|
478
477
|
}
|
|
479
|
-
return
|
|
478
|
+
return await interactiveLogin(store);
|
|
479
|
+
} catch (err) {
|
|
480
|
+
console.error(`
|
|
481
|
+
${core.getErrorMessage(err)}
|
|
482
|
+
`);
|
|
480
483
|
}
|
|
481
|
-
|
|
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;
|
|
482
533
|
}
|
|
483
534
|
}
|
|
484
535
|
|
|
@@ -491,8 +542,7 @@ async function handleCommand(tui, input2) {
|
|
|
491
542
|
const args = parts.slice(1);
|
|
492
543
|
switch (cmd) {
|
|
493
544
|
case "help":
|
|
494
|
-
tui
|
|
495
|
-
tui.requestRender();
|
|
545
|
+
showMessage(tui, formatHelpText());
|
|
496
546
|
return true;
|
|
497
547
|
case "login":
|
|
498
548
|
await handleLogin(tui);
|
|
@@ -512,255 +562,208 @@ async function handleCommand(tui, input2) {
|
|
|
512
562
|
case "contacts":
|
|
513
563
|
await handleListContacts(tui);
|
|
514
564
|
return true;
|
|
565
|
+
case "invite":
|
|
566
|
+
await handleInviteContact(tui, args[0]);
|
|
567
|
+
return true;
|
|
515
568
|
case "docs":
|
|
516
569
|
await handleListDocs(tui);
|
|
517
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;
|
|
518
577
|
case "conversations":
|
|
519
578
|
await handleListConversations(tui);
|
|
520
579
|
return true;
|
|
521
580
|
case "new":
|
|
522
581
|
handleNewConversation(tui);
|
|
523
582
|
return true;
|
|
583
|
+
case "models":
|
|
584
|
+
await handleModels(tui);
|
|
585
|
+
return true;
|
|
586
|
+
case "health":
|
|
587
|
+
await handleHealth(tui);
|
|
588
|
+
return true;
|
|
524
589
|
case "status":
|
|
525
590
|
handleStatus(tui);
|
|
526
591
|
return true;
|
|
592
|
+
case "logout":
|
|
593
|
+
handleLogout(tui);
|
|
594
|
+
return true;
|
|
527
595
|
case "exit":
|
|
528
596
|
case "quit":
|
|
529
597
|
tui.shutdown();
|
|
530
598
|
return true;
|
|
531
599
|
default:
|
|
532
|
-
tui.
|
|
533
|
-
`Unknown command: /${cmd}. Type /help for available commands.`,
|
|
534
|
-
"warning"
|
|
535
|
-
);
|
|
536
|
-
tui.requestRender();
|
|
600
|
+
showMessage(tui, `Unknown command: /${cmd}. Type /help for available commands.`, "warning");
|
|
537
601
|
return true;
|
|
538
602
|
}
|
|
539
603
|
}
|
|
540
604
|
async function handleLogin(tui) {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
605
|
+
const result = await runInteractiveFlow(
|
|
606
|
+
tui,
|
|
607
|
+
"Pausing TUI for login...",
|
|
608
|
+
() => interactiveLogin(tui.store),
|
|
609
|
+
"Login failed"
|
|
610
|
+
);
|
|
611
|
+
if (result) {
|
|
546
612
|
tui.setAuthContext(result.authContext);
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
tui.setWorkspaceContext(wsCtx);
|
|
550
|
-
tui.state.workspaceName = result.selectedWorkspaceName ?? null;
|
|
551
|
-
}
|
|
552
|
-
tui.restartTui();
|
|
553
|
-
tui.chatLog.addSystem("Logged in successfully.");
|
|
554
|
-
tui.requestRender();
|
|
555
|
-
} catch (err) {
|
|
556
|
-
tui.restartTui();
|
|
557
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
558
|
-
tui.chatLog.addSystem(`Login failed: ${msg}`, "error");
|
|
559
|
-
tui.requestRender();
|
|
613
|
+
await applyWorkspaceSelection(tui, result.selectedWorkspaceId, result.selectedWorkspaceName);
|
|
614
|
+
showMessage(tui, "Logged in successfully.");
|
|
560
615
|
}
|
|
561
616
|
}
|
|
562
617
|
async function handleRegister(tui) {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
tui.state.workspaceName = result.selectedWorkspaceName ?? null;
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
tui.restartTui();
|
|
577
|
-
tui.chatLog.addSystem(
|
|
578
|
-
result ? "Registered and logged in." : "Registered. Use /login to log in."
|
|
579
|
-
);
|
|
580
|
-
tui.requestRender();
|
|
581
|
-
} catch (err) {
|
|
582
|
-
tui.restartTui();
|
|
583
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
584
|
-
tui.chatLog.addSystem(`Registration failed: ${msg}`, "error");
|
|
585
|
-
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.");
|
|
586
628
|
}
|
|
587
629
|
}
|
|
588
630
|
async function handleCreateWorkspace(tui, name) {
|
|
589
|
-
if (!tui
|
|
590
|
-
tui.chatLog.addSystem("Not authenticated. Use /login first.", "error");
|
|
591
|
-
tui.requestRender();
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
631
|
+
if (!requireAuth(tui)) return;
|
|
594
632
|
if (!name.trim()) {
|
|
595
|
-
tui
|
|
596
|
-
tui.requestRender();
|
|
633
|
+
showMessage(tui, "Usage: /create <workspace name>", "warning");
|
|
597
634
|
return;
|
|
598
635
|
}
|
|
599
636
|
try {
|
|
600
|
-
tui
|
|
601
|
-
tui.requestRender();
|
|
637
|
+
showMessage(tui, `Creating workspace "${name}"...`);
|
|
602
638
|
const ws = await core.workspaces.createWorkspace(tui.authContext.arbi, name.trim());
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
tui.
|
|
606
|
-
|
|
607
|
-
tui.authContext.loginResult.serverSessionKey,
|
|
608
|
-
tui.store.requireCredentials().signingPrivateKeyBase64
|
|
609
|
-
);
|
|
610
|
-
tui.state.workspaceId = selected.external_id;
|
|
611
|
-
tui.state.workspaceName = selected.name;
|
|
612
|
-
tui.state.conversationMessageId = null;
|
|
613
|
-
tui.store.updateConfig({ selectedWorkspaceId: selected.external_id });
|
|
614
|
-
await tui.refreshWorkspaceContext();
|
|
615
|
-
tui.chatLog.addSystem(`Created and switched to workspace: ${colors.accentBold(selected.name)}`);
|
|
616
|
-
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
|
+
}
|
|
617
643
|
} catch (err) {
|
|
618
|
-
|
|
619
|
-
tui.chatLog.addSystem(`Failed to create workspace: ${msg}`, "error");
|
|
620
|
-
tui.requestRender();
|
|
644
|
+
showMessage(tui, `Failed to create workspace: ${core.getErrorMessage(err)}`, "error");
|
|
621
645
|
}
|
|
622
646
|
}
|
|
623
647
|
async function handleWorkspaceSwitch(tui, workspaceId) {
|
|
624
|
-
if (!tui.
|
|
625
|
-
tui.chatLog.addSystem("Not authenticated. Please restart and log in.", "error");
|
|
626
|
-
tui.requestRender();
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
648
|
+
if (!requireAuth(tui, "Not authenticated. Please restart and log in.")) return;
|
|
629
649
|
if (!workspaceId) {
|
|
630
650
|
await handleListWorkspaces(tui);
|
|
631
|
-
tui
|
|
632
|
-
tui.requestRender();
|
|
651
|
+
showMessage(tui, "Use /workspace <id> to switch.");
|
|
633
652
|
return;
|
|
634
653
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
const ws = await selectWorkspaceById(
|
|
640
|
-
tui.authContext.arbi,
|
|
641
|
-
workspaceId,
|
|
642
|
-
tui.authContext.loginResult.serverSessionKey,
|
|
643
|
-
tui.store.requireCredentials().signingPrivateKeyBase64
|
|
644
|
-
);
|
|
645
|
-
tui.state.workspaceId = ws.external_id;
|
|
646
|
-
tui.state.workspaceName = ws.name;
|
|
647
|
-
tui.state.conversationMessageId = null;
|
|
648
|
-
tui.store.updateConfig({ selectedWorkspaceId: ws.external_id });
|
|
649
|
-
await tui.refreshWorkspaceContext();
|
|
650
|
-
tui.chatLog.addSystem(`Switched to workspace: ${colors.accentBold(ws.name)}`);
|
|
651
|
-
tui.requestRender();
|
|
652
|
-
} catch (err) {
|
|
653
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
654
|
-
tui.chatLog.addSystem(`Failed to switch workspace: ${msg}`, "error");
|
|
655
|
-
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)}`);
|
|
656
658
|
}
|
|
657
659
|
}
|
|
658
660
|
async function handleListWorkspaces(tui) {
|
|
659
|
-
if (!tui.
|
|
660
|
-
tui.chatLog.addSystem("Not authenticated.", "error");
|
|
661
|
-
tui.requestRender();
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
661
|
+
if (!requireAuth(tui, "Not authenticated.")) return;
|
|
664
662
|
try {
|
|
665
663
|
const wsList = await core.workspaces.listWorkspaces(tui.authContext.arbi);
|
|
666
664
|
const lines = wsList.map((ws) => {
|
|
667
665
|
const current = ws.external_id === tui.state.workspaceId ? colors.accent(" (current)") : "";
|
|
668
666
|
return ` ${ws.external_id} ${ws.name}${current}`;
|
|
669
667
|
});
|
|
670
|
-
tui
|
|
671
|
-
tui.requestRender();
|
|
668
|
+
showMessage(tui, ["Workspaces:", "", ...lines].join("\n"));
|
|
672
669
|
} catch (err) {
|
|
673
|
-
|
|
674
|
-
tui.chatLog.addSystem(`Failed to list workspaces: ${msg}`, "error");
|
|
675
|
-
tui.requestRender();
|
|
670
|
+
showMessage(tui, `Failed to list workspaces: ${core.getErrorMessage(err)}`, "error");
|
|
676
671
|
}
|
|
677
672
|
}
|
|
678
673
|
async function handleListDocs(tui) {
|
|
679
674
|
if (!tui.authContext || !tui.state.workspaceId) {
|
|
680
|
-
tui
|
|
681
|
-
tui.requestRender();
|
|
675
|
+
showMessage(tui, "No workspace selected. Use /workspace <id> first.", "warning");
|
|
682
676
|
return;
|
|
683
677
|
}
|
|
684
678
|
try {
|
|
685
679
|
const docs = await core.documents.listDocuments(tui.authContext.arbi, tui.state.workspaceId);
|
|
686
680
|
if (docs.length === 0) {
|
|
687
|
-
tui
|
|
681
|
+
showMessage(tui, "No documents in this workspace.");
|
|
688
682
|
} else {
|
|
689
683
|
const lines = docs.map((d) => ` ${d.external_id} ${d.file_name ?? "(unnamed)"}`);
|
|
690
|
-
tui
|
|
684
|
+
showMessage(tui, [`Documents (${docs.length}):`, "", ...lines].join("\n"));
|
|
691
685
|
}
|
|
692
|
-
tui.requestRender();
|
|
693
686
|
} catch (err) {
|
|
694
|
-
|
|
695
|
-
tui.chatLog.addSystem(`Failed to list documents: ${msg}`, "error");
|
|
696
|
-
tui.requestRender();
|
|
687
|
+
showMessage(tui, `Failed to list documents: ${core.getErrorMessage(err)}`, "error");
|
|
697
688
|
}
|
|
698
689
|
}
|
|
699
690
|
async function handleListContacts(tui) {
|
|
700
|
-
if (!tui
|
|
701
|
-
tui.chatLog.addSystem("Not authenticated. Use /login first.", "error");
|
|
702
|
-
tui.requestRender();
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
691
|
+
if (!requireAuth(tui)) return;
|
|
705
692
|
try {
|
|
706
693
|
const contactList = await core.contacts.listContacts(tui.authContext.arbi);
|
|
707
|
-
const registered =
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
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.");
|
|
711
697
|
} else {
|
|
712
698
|
const lines = [];
|
|
713
699
|
if (registered.length > 0) {
|
|
714
700
|
lines.push(`Contacts (${registered.length}):`, "");
|
|
715
701
|
for (const c of registered) {
|
|
716
|
-
const name =
|
|
702
|
+
const name = core.formatUserName(c.user);
|
|
717
703
|
const nameStr = name ? colors.muted(` (${name})`) : "";
|
|
718
704
|
lines.push(` ${c.email}${nameStr}`);
|
|
719
705
|
}
|
|
720
706
|
}
|
|
721
|
-
if (
|
|
707
|
+
if (pending.length > 0) {
|
|
722
708
|
if (lines.length > 0) lines.push("");
|
|
723
|
-
lines.push(`Pending invitations (${
|
|
724
|
-
for (const c of
|
|
709
|
+
lines.push(`Pending invitations (${pending.length}):`);
|
|
710
|
+
for (const c of pending) {
|
|
725
711
|
lines.push(` ${c.email} \u2014 ${colors.muted(c.status)}`);
|
|
726
712
|
}
|
|
727
713
|
}
|
|
728
714
|
lines.push("", colors.muted("Type @name to start a DM with a registered contact."));
|
|
729
|
-
tui
|
|
715
|
+
showMessage(tui, lines.join("\n"));
|
|
730
716
|
}
|
|
731
|
-
tui.requestRender();
|
|
732
717
|
} catch (err) {
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
718
|
+
showMessage(tui, `Failed to list contacts: ${core.getErrorMessage(err)}`, "error");
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
async function handleInviteContact(tui, email) {
|
|
722
|
+
if (!requireAuth(tui)) return;
|
|
723
|
+
if (!email?.trim()) {
|
|
724
|
+
showMessage(tui, "Usage: /invite user@example.com", "warning");
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
try {
|
|
728
|
+
showMessage(tui, `Inviting ${email}...`);
|
|
729
|
+
const result = await core.contacts.addContacts(tui.authContext.arbi, [email.trim()]);
|
|
730
|
+
const contact = result[0];
|
|
731
|
+
if (contact?.status === "registered") {
|
|
732
|
+
showMessage(
|
|
733
|
+
tui,
|
|
734
|
+
`${colors.success("Added")} ${email} \u2014 already registered. You can DM them with @${email.split("@")[0]}`
|
|
735
|
+
);
|
|
736
|
+
} else {
|
|
737
|
+
showMessage(
|
|
738
|
+
tui,
|
|
739
|
+
`${colors.success("Invited")} ${email} \u2014 invitation sent. They'll appear in /contacts once they register.`
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
} catch (err) {
|
|
743
|
+
showMessage(tui, `Failed to invite contact: ${core.getErrorMessage(err)}`, "error");
|
|
736
744
|
}
|
|
737
745
|
}
|
|
738
746
|
async function handleListConversations(tui) {
|
|
739
747
|
if (!tui.authContext || !tui.state.workspaceId) {
|
|
740
|
-
tui
|
|
741
|
-
tui.requestRender();
|
|
748
|
+
showMessage(tui, "No workspace selected. Use /workspace <id> first.", "warning");
|
|
742
749
|
return;
|
|
743
750
|
}
|
|
744
751
|
try {
|
|
745
752
|
const convs = await core.conversations.listConversations(tui.authContext.arbi, tui.state.workspaceId);
|
|
746
753
|
if (convs.length === 0) {
|
|
747
|
-
tui
|
|
754
|
+
showMessage(tui, "No conversations in this workspace.");
|
|
748
755
|
} else {
|
|
749
756
|
const lines = convs.map((c) => ` ${c.external_id} ${c.title ?? "(untitled)"}`);
|
|
750
|
-
tui
|
|
757
|
+
showMessage(tui, [`Conversations (${convs.length}):`, "", ...lines].join("\n"));
|
|
751
758
|
}
|
|
752
|
-
tui.requestRender();
|
|
753
759
|
} catch (err) {
|
|
754
|
-
|
|
755
|
-
tui.chatLog.addSystem(`Failed to list conversations: ${msg}`, "error");
|
|
756
|
-
tui.requestRender();
|
|
760
|
+
showMessage(tui, `Failed to list conversations: ${core.getErrorMessage(err)}`, "error");
|
|
757
761
|
}
|
|
758
762
|
}
|
|
759
763
|
function handleNewConversation(tui) {
|
|
760
764
|
tui.state.conversationMessageId = null;
|
|
761
765
|
tui.store.clearChatSession();
|
|
762
|
-
tui
|
|
763
|
-
tui.requestRender();
|
|
766
|
+
showMessage(tui, "Started new conversation.");
|
|
764
767
|
}
|
|
765
768
|
function handleStatus(tui) {
|
|
766
769
|
const { state } = tui;
|
|
@@ -772,8 +775,112 @@ function handleStatus(tui) {
|
|
|
772
775
|
`Status: ${state.activityStatus}`,
|
|
773
776
|
`WebSocket: ${tui.wsConnected ? colors.success("connected") : colors.muted("disconnected")}`
|
|
774
777
|
];
|
|
775
|
-
tui
|
|
776
|
-
|
|
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
|
+
}
|
|
777
884
|
}
|
|
778
885
|
async function streamResponse(tui, response) {
|
|
779
886
|
tui.chatLog.startAssistant();
|
|
@@ -812,8 +919,7 @@ async function streamResponse(tui, response) {
|
|
|
812
919
|
} catch (err) {
|
|
813
920
|
tui.chatLog.finalizeAssistant();
|
|
814
921
|
tui.state.activityStatus = "error";
|
|
815
|
-
|
|
816
|
-
tui.chatLog.addSystem(`Streaming failed: ${msg}`, "error");
|
|
922
|
+
tui.chatLog.addSystem(`Streaming failed: ${core.getErrorMessage(err)}`, "error");
|
|
817
923
|
tui.requestRender();
|
|
818
924
|
throw err;
|
|
819
925
|
}
|
|
@@ -828,8 +934,7 @@ function deriveEncryptionKeypair(signingPrivateKeyBase64) {
|
|
|
828
934
|
}
|
|
829
935
|
async function resolveRecipient(tui, query) {
|
|
830
936
|
if (!tui.authContext) {
|
|
831
|
-
tui
|
|
832
|
-
tui.requestRender();
|
|
937
|
+
showMessage(tui, "Not authenticated. Use /login first.", "error");
|
|
833
938
|
return null;
|
|
834
939
|
}
|
|
835
940
|
const queryLower = query.toLowerCase();
|
|
@@ -844,21 +949,17 @@ async function resolveRecipient(tui, query) {
|
|
|
844
949
|
return emailPrefix === queryLower || givenName === queryLower;
|
|
845
950
|
});
|
|
846
951
|
if (matches.length === 0) {
|
|
847
|
-
|
|
952
|
+
showMessage(
|
|
953
|
+
tui,
|
|
848
954
|
`No registered contact matching "${query}". Use /contacts to see available contacts.`,
|
|
849
955
|
"warning"
|
|
850
956
|
);
|
|
851
|
-
tui.requestRender();
|
|
852
957
|
return null;
|
|
853
958
|
}
|
|
854
959
|
if (matches.length > 1) {
|
|
855
960
|
const names = matches.map((c) => ` ${c.email} (${c.user?.given_name ?? ""} ${c.user?.family_name ?? ""})`).join("\n");
|
|
856
|
-
tui.
|
|
857
|
-
|
|
858
|
-
${names}`,
|
|
859
|
-
"warning"
|
|
860
|
-
);
|
|
861
|
-
tui.requestRender();
|
|
961
|
+
showMessage(tui, `Ambiguous match for "${query}". Be more specific:
|
|
962
|
+
${names}`, "warning");
|
|
862
963
|
return null;
|
|
863
964
|
}
|
|
864
965
|
const contact = matches[0];
|
|
@@ -871,17 +972,14 @@ ${names}`,
|
|
|
871
972
|
family_name: user.family_name
|
|
872
973
|
};
|
|
873
974
|
} catch (err) {
|
|
874
|
-
|
|
875
|
-
tui.chatLog.addSystem(`Failed to resolve contact: ${msg}`, "error");
|
|
876
|
-
tui.requestRender();
|
|
975
|
+
showMessage(tui, `Failed to resolve contact: ${core.getErrorMessage(err)}`, "error");
|
|
877
976
|
return null;
|
|
878
977
|
}
|
|
879
978
|
}
|
|
880
979
|
async function handleChannelSwitch(tui, input2) {
|
|
881
980
|
const query = input2.slice(1).trim();
|
|
882
981
|
if (!query) {
|
|
883
|
-
tui
|
|
884
|
-
tui.requestRender();
|
|
982
|
+
showMessage(tui, "Usage: @username to switch to DM, @arbi to switch back.", "warning");
|
|
885
983
|
return;
|
|
886
984
|
}
|
|
887
985
|
if (query.toLowerCase() === "arbi") {
|
|
@@ -894,30 +992,24 @@ async function handleChannelSwitch(tui, input2) {
|
|
|
894
992
|
}
|
|
895
993
|
async function switchToDmChannel(tui, user) {
|
|
896
994
|
if (!tui.authContext || !tui.encryptionKeys) {
|
|
897
|
-
tui
|
|
898
|
-
tui.requestRender();
|
|
995
|
+
showMessage(tui, "Not authenticated or encryption not initialized.", "error");
|
|
899
996
|
return;
|
|
900
997
|
}
|
|
901
998
|
tui.currentDmChannel = { user };
|
|
902
999
|
tui.updateHeader();
|
|
903
1000
|
tui.chatLog.clearMessages();
|
|
904
|
-
tui.
|
|
905
|
-
`Switched to DM with ${colors.accent(user.email)}. Type @arbi to switch back.`
|
|
906
|
-
);
|
|
907
|
-
tui.requestRender();
|
|
1001
|
+
showMessage(tui, `Switched to DM with ${colors.accent(user.email)}. Type @arbi to switch back.`);
|
|
908
1002
|
await loadDmHistory(tui, user);
|
|
909
1003
|
}
|
|
910
1004
|
function switchToArbi(tui) {
|
|
911
1005
|
if (!tui.currentDmChannel) {
|
|
912
|
-
tui
|
|
913
|
-
tui.requestRender();
|
|
1006
|
+
showMessage(tui, "Already in AI chat mode.", "info");
|
|
914
1007
|
return;
|
|
915
1008
|
}
|
|
916
1009
|
tui.currentDmChannel = null;
|
|
917
1010
|
tui.updateHeader();
|
|
918
1011
|
tui.chatLog.clearMessages();
|
|
919
|
-
tui
|
|
920
|
-
tui.requestRender();
|
|
1012
|
+
showMessage(tui, "Switched back to AI chat.");
|
|
921
1013
|
}
|
|
922
1014
|
async function loadDmHistory(tui, user) {
|
|
923
1015
|
if (!tui.authContext || !tui.encryptionKeys) return;
|
|
@@ -927,9 +1019,7 @@ async function loadDmHistory(tui, user) {
|
|
|
927
1019
|
const conversation = allDms.filter(
|
|
928
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)
|
|
929
1021
|
);
|
|
930
|
-
conversation.sort(
|
|
931
|
-
(a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
|
|
932
|
-
);
|
|
1022
|
+
conversation.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
|
933
1023
|
const unreadIds = conversation.filter((msg) => !msg.read && msg.sender.external_id === user.external_id).map((msg) => msg.external_id);
|
|
934
1024
|
if (unreadIds.length > 0) {
|
|
935
1025
|
core.dm.markRead(tui.authContext.arbi, unreadIds).catch(() => {
|
|
@@ -944,15 +1034,12 @@ async function loadDmHistory(tui, user) {
|
|
|
944
1034
|
}
|
|
945
1035
|
tui.requestRender();
|
|
946
1036
|
} catch (err) {
|
|
947
|
-
|
|
948
|
-
tui.chatLog.addSystem(`Failed to load DM history: ${errMsg}`, "error");
|
|
949
|
-
tui.requestRender();
|
|
1037
|
+
showMessage(tui, `Failed to load DM history: ${core.getErrorMessage(err)}`, "error");
|
|
950
1038
|
}
|
|
951
1039
|
}
|
|
952
1040
|
async function sendEncryptedDm(tui, plaintext) {
|
|
953
1041
|
if (!tui.authContext || !tui.encryptionKeys || !tui.currentDmChannel) {
|
|
954
|
-
tui
|
|
955
|
-
tui.requestRender();
|
|
1042
|
+
showMessage(tui, "Cannot send DM \u2014 not in a DM channel.", "error");
|
|
956
1043
|
return;
|
|
957
1044
|
}
|
|
958
1045
|
const recipient = tui.currentDmChannel.user;
|
|
@@ -968,9 +1055,7 @@ async function sendEncryptedDm(tui, plaintext) {
|
|
|
968
1055
|
displayDmMessage(tui, plaintext, true, recipient.email);
|
|
969
1056
|
tui.requestRender();
|
|
970
1057
|
} catch (err) {
|
|
971
|
-
|
|
972
|
-
tui.chatLog.addSystem(`Failed to send DM: ${msg}`, "error");
|
|
973
|
-
tui.requestRender();
|
|
1058
|
+
showMessage(tui, `Failed to send DM: ${core.getErrorMessage(err)}`, "error");
|
|
974
1059
|
}
|
|
975
1060
|
}
|
|
976
1061
|
async function handleIncomingDm(tui, msg) {
|
|
@@ -1061,12 +1146,21 @@ function handleBatchComplete(msg, toasts) {
|
|
|
1061
1146
|
async function connectTuiWebSocket(options) {
|
|
1062
1147
|
const { baseUrl, accessToken, toasts, tui } = options;
|
|
1063
1148
|
try {
|
|
1064
|
-
const connection = await core.
|
|
1149
|
+
const connection = await core.connectWithReconnect({
|
|
1065
1150
|
baseUrl,
|
|
1066
1151
|
accessToken,
|
|
1067
1152
|
onMessage: (msg) => handleMessage(msg, toasts, tui ?? null),
|
|
1068
1153
|
onClose: (_code, _reason) => {
|
|
1069
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);
|
|
1070
1164
|
}
|
|
1071
1165
|
});
|
|
1072
1166
|
return connection;
|
|
@@ -1114,14 +1208,7 @@ var ArbiTui = class {
|
|
|
1114
1208
|
this.chatLog = new ChatLog();
|
|
1115
1209
|
this.toastContainer = new ToastContainer();
|
|
1116
1210
|
this.toastContainer.setRenderCallback(() => this.tui.requestRender());
|
|
1117
|
-
this.editor =
|
|
1118
|
-
this.editor.setAutocompleteProvider(new piTui.CombinedAutocompleteProvider(commands));
|
|
1119
|
-
this.editor.onSubmit = (text) => this.handleSubmit(text);
|
|
1120
|
-
this.editor.onEscape = () => this.handleAbort();
|
|
1121
|
-
this.editor.onCtrlC = () => this.shutdown();
|
|
1122
|
-
this.editor.onCtrlD = () => this.shutdown();
|
|
1123
|
-
this.editor.onCtrlW = () => this.handleSubmit("/workspaces");
|
|
1124
|
-
this.editor.onCtrlN = () => this.handleSubmit("/new");
|
|
1211
|
+
this.editor = this.createEditor();
|
|
1125
1212
|
this.tui.addChild(this.header);
|
|
1126
1213
|
this.tui.addChild(new piTui.Spacer(1));
|
|
1127
1214
|
this.tui.addChild(this.toastContainer);
|
|
@@ -1162,20 +1249,25 @@ var ArbiTui = class {
|
|
|
1162
1249
|
this.tui.addChild(new piTui.Spacer(1));
|
|
1163
1250
|
this.tui.addChild(this.toastContainer);
|
|
1164
1251
|
this.tui.addChild(this.chatLog);
|
|
1165
|
-
this.editor =
|
|
1166
|
-
this.editor.setAutocompleteProvider(new piTui.CombinedAutocompleteProvider(commands));
|
|
1167
|
-
this.editor.onSubmit = (text) => this.handleSubmit(text);
|
|
1168
|
-
this.editor.onEscape = () => this.handleAbort();
|
|
1169
|
-
this.editor.onCtrlC = () => this.shutdown();
|
|
1170
|
-
this.editor.onCtrlD = () => this.shutdown();
|
|
1171
|
-
this.editor.onCtrlW = () => this.handleSubmit("/workspaces");
|
|
1172
|
-
this.editor.onCtrlN = () => this.handleSubmit("/new");
|
|
1252
|
+
this.editor = this.createEditor();
|
|
1173
1253
|
this.tui.addChild(this.editor);
|
|
1174
1254
|
this.tui.setFocus(this.editor);
|
|
1175
1255
|
this.tui.start();
|
|
1176
1256
|
this.updateHeader();
|
|
1177
1257
|
this.tui.requestRender();
|
|
1178
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
|
+
}
|
|
1179
1271
|
/** Request a render update. */
|
|
1180
1272
|
requestRender() {
|
|
1181
1273
|
this.tui.requestRender();
|
|
@@ -1205,6 +1297,10 @@ var ArbiTui = class {
|
|
|
1205
1297
|
this.workspaceContext = ctx;
|
|
1206
1298
|
this.updateHeader();
|
|
1207
1299
|
}
|
|
1300
|
+
/** Public accessor for workspace context (used by command handlers). */
|
|
1301
|
+
get wsContext() {
|
|
1302
|
+
return this.workspaceContext;
|
|
1303
|
+
}
|
|
1208
1304
|
/** Whether the WebSocket is currently connected. */
|
|
1209
1305
|
get wsConnected() {
|
|
1210
1306
|
return this.wsConnection !== null;
|
|
@@ -1282,12 +1378,18 @@ var ArbiTui = class {
|
|
|
1282
1378
|
const result = await streamResponse(this, response);
|
|
1283
1379
|
if (result.assistantMessageExtId) {
|
|
1284
1380
|
this.state.conversationMessageId = result.assistantMessageExtId;
|
|
1285
|
-
|
|
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);
|
|
1286
1389
|
}
|
|
1287
1390
|
} catch (err) {
|
|
1288
1391
|
this.state.activityStatus = "error";
|
|
1289
|
-
|
|
1290
|
-
this.chatLog.addSystem(`Error: ${msg}`, "error");
|
|
1392
|
+
this.chatLog.addSystem(`Error: ${core.getErrorMessage(err)}`, "error");
|
|
1291
1393
|
}
|
|
1292
1394
|
this.state.activityStatus = "idle";
|
|
1293
1395
|
this.updateHeader();
|
|
@@ -1352,8 +1454,7 @@ program.name("arbi-tui").description("Interactive terminal UI for ARBI \u2014 ch
|
|
|
1352
1454
|
}
|
|
1353
1455
|
}
|
|
1354
1456
|
} catch (err) {
|
|
1355
|
-
|
|
1356
|
-
tui.chatLog.addSystem(`Failed to load workspace: ${msg}`, "warning");
|
|
1457
|
+
tui.chatLog.addSystem(`Failed to load workspace: ${core.getErrorMessage(err)}`, "warning");
|
|
1357
1458
|
tui.chatLog.addSystem("Use /workspaces to list and /workspace <id> to select.");
|
|
1358
1459
|
}
|
|
1359
1460
|
} else {
|
|
@@ -1365,6 +1466,29 @@ program.name("arbi-tui").description("Interactive terminal UI for ARBI \u2014 ch
|
|
|
1365
1466
|
if (session.lastMessageExtId) {
|
|
1366
1467
|
tui.state.conversationMessageId = session.lastMessageExtId;
|
|
1367
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
|
+
}
|
|
1368
1492
|
tui.chatLog.addSystem(
|
|
1369
1493
|
"Type a question to chat with the ARBI assistant. Use /help for commands."
|
|
1370
1494
|
);
|