@arbidocs/tui 0.1.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +406 -318
- 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));
|
|
@@ -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
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
store.
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
|
359
|
-
|
|
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
|
|
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
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
|
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
|
|
419
|
-
message: "Verification code",
|
|
420
|
-
validate: (v) => v.trim() ? true : "Required"
|
|
421
|
-
});
|
|
404
|
+
verificationCode = await promptInput("Verification code");
|
|
422
405
|
}
|
|
423
|
-
const pw = await
|
|
424
|
-
|
|
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
|
|
437
|
-
const lastName = await
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
|
478
|
+
return await interactiveLogin(store);
|
|
479
|
+
} catch (err) {
|
|
480
|
+
console.error(`
|
|
481
|
+
${core.getErrorMessage(err)}
|
|
482
|
+
`);
|
|
481
483
|
}
|
|
482
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
552
|
-
|
|
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
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
|
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
|
|
600
|
-
tui.requestRender();
|
|
633
|
+
showMessage(tui, "Usage: /create <workspace name>", "warning");
|
|
601
634
|
return;
|
|
602
635
|
}
|
|
603
636
|
try {
|
|
604
|
-
tui
|
|
605
|
-
tui.requestRender();
|
|
637
|
+
showMessage(tui, `Creating workspace "${name}"...`);
|
|
606
638
|
const ws = await core.workspaces.createWorkspace(tui.authContext.arbi, name.trim());
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
tui.
|
|
610
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
636
|
-
tui.requestRender();
|
|
651
|
+
showMessage(tui, "Use /workspace <id> to switch.");
|
|
637
652
|
return;
|
|
638
653
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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.
|
|
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
|
|
675
|
-
tui.requestRender();
|
|
668
|
+
showMessage(tui, ["Workspaces:", "", ...lines].join("\n"));
|
|
676
669
|
} catch (err) {
|
|
677
|
-
|
|
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
|
|
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
|
|
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
|
|
684
|
+
showMessage(tui, [`Documents (${docs.length}):`, "", ...lines].join("\n"));
|
|
695
685
|
}
|
|
696
|
-
tui.requestRender();
|
|
697
686
|
} catch (err) {
|
|
698
|
-
|
|
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
|
|
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 =
|
|
712
|
-
|
|
713
|
-
|
|
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 =
|
|
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 (
|
|
707
|
+
if (pending.length > 0) {
|
|
726
708
|
if (lines.length > 0) lines.push("");
|
|
727
|
-
lines.push(`Pending invitations (${
|
|
728
|
-
for (const c of
|
|
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
|
|
715
|
+
showMessage(tui, lines.join("\n"));
|
|
734
716
|
}
|
|
735
|
-
tui.requestRender();
|
|
736
717
|
} catch (err) {
|
|
737
|
-
|
|
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
|
|
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
|
|
750
|
-
tui.requestRender();
|
|
724
|
+
showMessage(tui, "Usage: /invite user@example.com", "warning");
|
|
751
725
|
return;
|
|
752
726
|
}
|
|
753
727
|
try {
|
|
754
|
-
tui
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
757
|
+
showMessage(tui, [`Conversations (${convs.length}):`, "", ...lines].join("\n"));
|
|
787
758
|
}
|
|
788
|
-
tui.requestRender();
|
|
789
759
|
} catch (err) {
|
|
790
|
-
|
|
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
|
|
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
|
|
812
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
893
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
);
|