@arbidocs/tui 0.3.16 → 0.3.17

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.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- var commander = require('commander');
5
4
  var sdk = require('@arbidocs/sdk');
5
+ var commander = require('commander');
6
6
  var piTui = require('@mariozechner/pi-tui');
7
7
  var chalk = require('chalk');
8
8
  require('fake-indexeddb/auto');
@@ -13,6 +13,78 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
13
13
 
14
14
  var chalk__default = /*#__PURE__*/_interopDefault(chalk);
15
15
 
16
+ var __defProp = Object.defineProperty;
17
+ var __getOwnPropNames = Object.getOwnPropertyNames;
18
+ var __esm = (fn, res) => function __init() {
19
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
20
+ };
21
+ var __export = (target, all) => {
22
+ for (var name in all)
23
+ __defProp(target, name, { get: all[name], enumerable: true });
24
+ };
25
+
26
+ // src/tui-helpers.ts
27
+ var tui_helpers_exports = {};
28
+ __export(tui_helpers_exports, {
29
+ applyWorkspaceSelection: () => applyWorkspaceSelection,
30
+ requireAuth: () => requireAuth,
31
+ runInteractiveFlow: () => runInteractiveFlow,
32
+ showMessage: () => showMessage,
33
+ switchWorkspace: () => switchWorkspace
34
+ });
35
+ function requireAuth(tui, message = "Not authenticated. Use /login first.") {
36
+ if (tui.authContext) return true;
37
+ showMessage(tui, message, "error");
38
+ return false;
39
+ }
40
+ function showMessage(tui, message, level) {
41
+ tui.chatLog.addSystem(message, level);
42
+ tui.requestRender();
43
+ }
44
+ async function switchWorkspace(tui, workspaceId) {
45
+ if (!tui.authContext) return null;
46
+ try {
47
+ const ws = await sdk.selectWorkspaceById(
48
+ tui.authContext.arbi,
49
+ workspaceId,
50
+ tui.authContext.loginResult.serverSessionKey,
51
+ tui.store.requireCredentials().signingPrivateKeyBase64
52
+ );
53
+ tui.state.workspaceId = ws.external_id;
54
+ tui.state.workspaceName = ws.name;
55
+ tui.state.conversationMessageId = null;
56
+ tui.store.clearChatSession();
57
+ tui.store.updateConfig({ selectedWorkspaceId: ws.external_id });
58
+ await tui.refreshWorkspaceContext();
59
+ return ws;
60
+ } catch (err) {
61
+ showMessage(tui, `Failed to switch workspace: ${sdk.getErrorMessage(err)}`, "error");
62
+ return null;
63
+ }
64
+ }
65
+ async function applyWorkspaceSelection(tui, workspaceId, workspaceName) {
66
+ if (!workspaceId) return;
67
+ const wsCtx = await sdk.resolveWorkspace(tui.store, workspaceId);
68
+ tui.setWorkspaceContext(wsCtx);
69
+ tui.state.workspaceName = workspaceName ?? null;
70
+ }
71
+ async function runInteractiveFlow(tui, pauseMessage, action, errorPrefix) {
72
+ showMessage(tui, pauseMessage);
73
+ tui.stopTui();
74
+ try {
75
+ const result = await action();
76
+ tui.restartTui();
77
+ return result;
78
+ } catch (err) {
79
+ tui.restartTui();
80
+ showMessage(tui, `${errorPrefix}: ${sdk.getErrorMessage(err)}`, "error");
81
+ return null;
82
+ }
83
+ }
84
+ var init_tui_helpers = __esm({
85
+ "src/tui-helpers.ts"() {
86
+ }
87
+ });
16
88
  var colors = {
17
89
  /** Primary accent — teal/cyan used for headings, prompts, highlights */
18
90
  accent: chalk__default.default.cyan,
@@ -99,7 +171,7 @@ var AssistantMessage = class extends piTui.Container {
99
171
  label;
100
172
  constructor() {
101
173
  super();
102
- this.label = new piTui.Text(colors.assistantLabel("Assistant"), 1, 0);
174
+ this.label = new piTui.Text(colors.assistantLabel("ARBI"), 1, 0);
103
175
  this.markdown = new piTui.Markdown("", 1, 0, assistantMarkdownTheme);
104
176
  this.addChild(this.label);
105
177
  this.addChild(this.markdown);
@@ -124,6 +196,23 @@ var SystemMessage = class extends piTui.Text {
124
196
  super(styleFn(message), 1, 0);
125
197
  }
126
198
  };
199
+ var RightAlignedText = class {
200
+ text;
201
+ styleFn;
202
+ constructor(text, styleFn) {
203
+ this.text = text;
204
+ this.styleFn = styleFn;
205
+ }
206
+ invalidate() {
207
+ }
208
+ render(width) {
209
+ if (!this.text || this.text.trim() === "") return [];
210
+ const styled = this.styleFn(this.text);
211
+ const textWidth = piTui.visibleWidth(styled);
212
+ const padding = Math.max(0, width - textWidth);
213
+ return [" ".repeat(padding) + styled];
214
+ }
215
+ };
127
216
  var AgentStep = class extends piTui.Text {
128
217
  completed = false;
129
218
  constructor(focus) {
@@ -160,6 +249,11 @@ var ChatLog = class extends piTui.Container {
160
249
  this.addChild(new SystemMessage(message, level));
161
250
  this.addChild(new piTui.Spacer(1));
162
251
  }
252
+ /** Add a right-aligned summary line (e.g. stream stats). */
253
+ addSummary(text) {
254
+ this.addChild(new RightAlignedText(text, colors.systemInfo));
255
+ this.addChild(new piTui.Spacer(1));
256
+ }
163
257
  /** Add a received DM message (cyan label with sender email). */
164
258
  addDm(senderEmail, text) {
165
259
  const container = new piTui.Container();
@@ -195,13 +289,17 @@ var ChatLog = class extends piTui.Container {
195
289
  this.activeSteps.push(step);
196
290
  this.addChild(step);
197
291
  }
198
- /** Finalize the current assistant response. */
199
- finalizeAssistant() {
292
+ /** Remove all active agent steps from the chat log (e.g. when streaming starts). */
293
+ clearActiveSteps() {
200
294
  for (const step of this.activeSteps) {
201
- step.complete();
295
+ this.removeChild(step);
202
296
  }
203
- this.activeAssistant = null;
204
297
  this.activeSteps = [];
298
+ }
299
+ /** Finalize the current assistant response. */
300
+ finalizeAssistant() {
301
+ this.clearActiveSteps();
302
+ this.activeAssistant = null;
205
303
  this.addChild(new piTui.Spacer(1));
206
304
  }
207
305
  };
@@ -306,30 +404,32 @@ var ToastContainer = class extends piTui.Container {
306
404
  }
307
405
  };
308
406
 
309
- // src/commands.ts
310
- var commands = [
311
- { name: "help", description: "Show available commands" },
312
- { name: "login", description: "Log in (re-authenticate)" },
313
- { name: "register", description: "Register a new account" },
314
- { name: "create", description: "Create a new workspace: /create <name>" },
315
- { name: "workspace", description: "Switch workspace: /workspace <id>" },
316
- { name: "workspaces", description: "List all workspaces" },
317
- { name: "contacts", description: "List contacts (type @name to DM)" },
318
- { name: "invite", description: "Invite a contact: /invite user@example.com" },
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>" },
322
- { name: "conversations", description: "List conversations" },
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" },
326
- { name: "status", description: "Show auth/workspace/connection status" },
327
- { name: "logout", description: "Log out and clear credentials" },
328
- { name: "exit", description: "Exit TUI" },
329
- { name: "quit", description: "Exit TUI" }
330
- ];
407
+ // src/command-registry.ts
408
+ init_tui_helpers();
409
+ var registry = /* @__PURE__ */ new Map();
410
+ function registerCommand(def) {
411
+ registry.set(def.name, def);
412
+ }
413
+ function registerCommands(defs) {
414
+ for (const def of defs) {
415
+ registerCommand(def);
416
+ }
417
+ }
418
+ function toSlashCommands() {
419
+ const cmds = [];
420
+ for (const def of registry.values()) {
421
+ if (def.hidden) continue;
422
+ cmds.push({ name: def.name, description: def.description });
423
+ }
424
+ return cmds;
425
+ }
331
426
  function formatHelpText() {
332
- const lines = commands.filter((c) => c.name !== "quit").map((c) => ` /${c.name} \u2014 ${c.description}`);
427
+ const lines = [];
428
+ for (const def of registry.values()) {
429
+ if (def.hidden) continue;
430
+ const hint = def.argHint ? ` <${def.argHint}>` : "";
431
+ lines.push(` /${def.name}${hint} \u2014 ${def.description}`);
432
+ }
333
433
  return [
334
434
  "Available commands:",
335
435
  "",
@@ -346,6 +446,133 @@ function formatHelpText() {
346
446
  " Escape \u2014 Abort streaming"
347
447
  ].join("\n");
348
448
  }
449
+ async function dispatchCommand(tui, input2) {
450
+ const trimmed = input2.trim();
451
+ if (!trimmed.startsWith("/")) return false;
452
+ const parts = trimmed.slice(1).split(/\s+/);
453
+ const cmdName = parts[0]?.toLowerCase();
454
+ const args = parts.slice(1);
455
+ if (!cmdName) return false;
456
+ const def = registry.get(cmdName);
457
+ if (!def) {
458
+ showMessage(tui, `Unknown command: /${cmdName}. Type /help for available commands.`, "warning");
459
+ return true;
460
+ }
461
+ if (def.minArgs && args.length < def.minArgs) {
462
+ const hint = def.argHint ? ` <${def.argHint}>` : "";
463
+ showMessage(tui, `Usage: /${def.name}${hint}`, "warning");
464
+ return true;
465
+ }
466
+ let ctx;
467
+ if (def.requires === "none") {
468
+ ctx = { requires: "none", args, rawInput: trimmed, tui };
469
+ } else if (def.requires === "auth") {
470
+ if (!tui.authContext) {
471
+ showMessage(tui, "Not authenticated. Use /login first.", "error");
472
+ return true;
473
+ }
474
+ ctx = {
475
+ requires: "auth",
476
+ args,
477
+ rawInput: trimmed,
478
+ tui,
479
+ arbi: tui.authContext.arbi,
480
+ authContext: tui.authContext
481
+ };
482
+ } else {
483
+ if (!tui.authContext) {
484
+ showMessage(tui, "Not authenticated. Use /login first.", "error");
485
+ return true;
486
+ }
487
+ if (!tui.wsContext) {
488
+ showMessage(tui, "No workspace selected. Use /workspace <id> first.", "warning");
489
+ return true;
490
+ }
491
+ ctx = {
492
+ requires: "workspace",
493
+ args,
494
+ rawInput: trimmed,
495
+ tui,
496
+ arbi: tui.wsContext.arbi,
497
+ authContext: tui.authContext,
498
+ wsContext: tui.wsContext,
499
+ authHeaders: {
500
+ baseUrl: tui.wsContext.config.baseUrl,
501
+ accessToken: tui.wsContext.accessToken,
502
+ workspaceKeyHeader: tui.wsContext.workspaceKeyHeader
503
+ }
504
+ };
505
+ }
506
+ try {
507
+ const result = await def.run(ctx);
508
+ if (typeof result === "string") {
509
+ showMessage(tui, result);
510
+ } else if (Array.isArray(result)) {
511
+ showMessage(tui, result.join("\n"));
512
+ }
513
+ } catch (err) {
514
+ showMessage(tui, `Command /${def.name} failed: ${sdk.getErrorMessage(err)}`, "error");
515
+ }
516
+ return true;
517
+ }
518
+
519
+ // src/commands/general.ts
520
+ var generalCommands = [
521
+ {
522
+ name: "help",
523
+ description: "Show available commands",
524
+ requires: "none",
525
+ run: () => formatHelpText()
526
+ },
527
+ {
528
+ name: "status",
529
+ description: "Show auth/workspace/connection status",
530
+ requires: "none",
531
+ run: (ctx) => {
532
+ const { tui } = ctx;
533
+ const { state } = tui;
534
+ return [
535
+ `Authenticated: ${state.isAuthenticated ? colors.success("yes") : colors.error("no")}`,
536
+ `Workspace: ${state.workspaceName ? colors.accent(state.workspaceName) : colors.muted("none")}`,
537
+ `Workspace ID: ${state.workspaceId ?? colors.muted("none")}`,
538
+ `Conversation: ${state.conversationMessageId ? colors.muted(state.conversationMessageId) : colors.muted("new")}`,
539
+ `Status: ${state.activityStatus}`,
540
+ `WebSocket: ${tui.wsConnected ? colors.success("connected") : colors.muted("disconnected")}`
541
+ ];
542
+ }
543
+ },
544
+ {
545
+ name: "new",
546
+ description: "Start fresh conversation (clear threading)",
547
+ requires: "none",
548
+ run: (ctx) => {
549
+ const { tui } = ctx;
550
+ tui.state.conversationMessageId = null;
551
+ tui.lastMetadata = null;
552
+ tui.store.clearChatSession();
553
+ return "Started new conversation.";
554
+ }
555
+ },
556
+ {
557
+ name: "exit",
558
+ description: "Exit TUI",
559
+ requires: "none",
560
+ run: (ctx) => {
561
+ const { tui } = ctx;
562
+ tui.shutdown();
563
+ }
564
+ },
565
+ {
566
+ name: "quit",
567
+ description: "Exit TUI",
568
+ requires: "none",
569
+ hidden: true,
570
+ run: (ctx) => {
571
+ const { tui } = ctx;
572
+ tui.shutdown();
573
+ }
574
+ }
575
+ ];
349
576
  async function promptSelect(message, choices) {
350
577
  return prompts.select({ message, choices });
351
578
  }
@@ -483,218 +710,396 @@ ${sdk.getErrorMessage(err)}
483
710
  }
484
711
  }
485
712
  }
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 sdk.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: ${sdk.getErrorMessage(err)}`, "error");
513
- return null;
514
- }
515
- }
516
- async function applyWorkspaceSelection(tui, workspaceId, workspaceName) {
517
- if (!workspaceId) return;
518
- const wsCtx = await sdk.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}: ${sdk.getErrorMessage(err)}`, "error");
532
- return null;
533
- }
534
- }
535
713
 
536
- // src/command-handlers.ts
537
- async function handleCommand(tui, input2) {
538
- const trimmed = input2.trim();
539
- if (!trimmed.startsWith("/")) return false;
540
- const parts = trimmed.slice(1).split(/\s+/);
541
- const cmd = parts[0]?.toLowerCase();
542
- const args = parts.slice(1);
543
- switch (cmd) {
544
- case "help":
545
- showMessage(tui, formatHelpText());
546
- return true;
547
- case "login":
548
- await handleLogin(tui);
549
- return true;
550
- case "register":
551
- await handleRegister(tui);
552
- return true;
553
- case "create":
554
- await handleCreateWorkspace(tui, args.join(" "));
555
- return true;
556
- case "workspace":
557
- await handleWorkspaceSwitch(tui, args[0]);
558
- return true;
559
- case "workspaces":
560
- await handleListWorkspaces(tui);
561
- return true;
562
- case "contacts":
563
- await handleListContacts(tui);
564
- return true;
565
- case "invite":
566
- await handleInviteContact(tui, args[0]);
567
- return true;
568
- case "docs":
569
- await handleListDocs(tui);
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;
577
- case "conversations":
578
- await handleListConversations(tui);
579
- return true;
580
- case "new":
581
- handleNewConversation(tui);
582
- return true;
583
- case "models":
584
- await handleModels(tui);
585
- return true;
586
- case "health":
587
- await handleHealth(tui);
588
- return true;
589
- case "status":
590
- handleStatus(tui);
591
- return true;
592
- case "logout":
593
- handleLogout(tui);
594
- return true;
595
- case "exit":
596
- case "quit":
597
- tui.shutdown();
598
- return true;
599
- default:
600
- showMessage(tui, `Unknown command: /${cmd}. Type /help for available commands.`, "warning");
601
- return true;
602
- }
603
- }
604
- async function handleLogin(tui) {
605
- const result = await runInteractiveFlow(
606
- tui,
607
- "Pausing TUI for login...",
608
- () => interactiveLogin(tui.store),
609
- "Login failed"
610
- );
611
- if (result) {
612
- tui.setAuthContext(result.authContext);
613
- await applyWorkspaceSelection(tui, result.selectedWorkspaceId, result.selectedWorkspaceName);
614
- showMessage(tui, "Logged in successfully.");
615
- }
616
- }
617
- async function handleRegister(tui) {
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.");
628
- }
629
- }
630
- async function handleCreateWorkspace(tui, name) {
631
- if (!requireAuth(tui)) return;
632
- if (!name.trim()) {
633
- showMessage(tui, "Usage: /create <workspace name>", "warning");
634
- return;
635
- }
636
- try {
637
- showMessage(tui, `Creating workspace "${name}"...`);
638
- const ws = await sdk.workspaces.createWorkspace(tui.authContext.arbi, name.trim());
639
- const selected = await switchWorkspace(tui, ws.external_id);
640
- if (selected) {
641
- showMessage(tui, `Created and switched to workspace: ${colors.accentBold(selected.name)}`);
714
+ // src/commands/auth.ts
715
+ init_tui_helpers();
716
+ var authCommands = [
717
+ {
718
+ name: "login",
719
+ description: "Log in (re-authenticate)",
720
+ requires: "none",
721
+ run: async (ctx) => {
722
+ const { tui } = ctx;
723
+ const result = await runInteractiveFlow(
724
+ tui,
725
+ "Pausing TUI for login...",
726
+ () => interactiveLogin(tui.store),
727
+ "Login failed"
728
+ );
729
+ if (result) {
730
+ tui.setAuthContext(result.authContext);
731
+ await applyWorkspaceSelection(tui, result.selectedWorkspaceId, result.selectedWorkspaceName);
732
+ showMessage(tui, "Logged in successfully.");
733
+ }
734
+ }
735
+ },
736
+ {
737
+ name: "register",
738
+ description: "Register a new account",
739
+ requires: "none",
740
+ run: async (ctx) => {
741
+ const { tui } = ctx;
742
+ const result = await runInteractiveFlow(
743
+ tui,
744
+ "Pausing TUI for registration...",
745
+ () => interactiveRegister(tui.store),
746
+ "Registration failed"
747
+ );
748
+ if (result) {
749
+ tui.setAuthContext(result.authContext);
750
+ await applyWorkspaceSelection(tui, result.selectedWorkspaceId, result.selectedWorkspaceName);
751
+ showMessage(tui, "Registered and logged in.");
752
+ }
753
+ }
754
+ },
755
+ {
756
+ name: "logout",
757
+ description: "Log out and clear credentials",
758
+ requires: "none",
759
+ run: (ctx) => {
760
+ const { tui } = ctx;
761
+ tui.store.deleteCredentials();
762
+ tui.store.updateConfig({ selectedWorkspaceId: void 0 });
763
+ tui.store.clearChatSession();
764
+ tui.authContext = null;
765
+ tui.state.isAuthenticated = false;
766
+ tui.state.workspaceId = null;
767
+ tui.state.workspaceName = null;
768
+ tui.state.conversationMessageId = null;
769
+ return "Logged out. Use /login to authenticate again.";
642
770
  }
643
- } catch (err) {
644
- showMessage(tui, `Failed to create workspace: ${sdk.getErrorMessage(err)}`, "error");
645
- }
646
- }
647
- async function handleWorkspaceSwitch(tui, workspaceId) {
648
- if (!requireAuth(tui, "Not authenticated. Please restart and log in.")) return;
649
- if (!workspaceId) {
650
- await handleListWorkspaces(tui);
651
- showMessage(tui, "Use /workspace <id> to switch.");
652
- return;
653
771
  }
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)}`);
772
+ ];
773
+ init_tui_helpers();
774
+ var workspaceCommands = [
775
+ {
776
+ name: "workspaces",
777
+ description: "List all workspaces",
778
+ requires: "auth",
779
+ run: async (ctx) => {
780
+ const { arbi, tui } = ctx;
781
+ const wsList = await sdk.workspaces.listWorkspaces(arbi);
782
+ const lines = wsList.map((ws) => {
783
+ const current = ws.external_id === tui.state.workspaceId ? colors.accent(" (current)") : "";
784
+ return ` ${ws.external_id} ${ws.name}${current}`;
785
+ });
786
+ return ["Workspaces:", "", ...lines];
787
+ }
788
+ },
789
+ {
790
+ name: "workspace",
791
+ description: "Switch workspace",
792
+ argHint: "id",
793
+ requires: "auth",
794
+ run: async (ctx) => {
795
+ const { args, arbi, tui } = ctx;
796
+ if (!args[0]) {
797
+ const wsList = await sdk.workspaces.listWorkspaces(arbi);
798
+ const lines = wsList.map((ws2) => {
799
+ const current = ws2.external_id === tui.state.workspaceId ? colors.accent(" (current)") : "";
800
+ return ` ${ws2.external_id} ${ws2.name}${current}`;
801
+ });
802
+ showMessage(tui, ["Workspaces:", "", ...lines].join("\n"));
803
+ return "Use /workspace <id> to switch.";
804
+ }
805
+ showMessage(tui, `Switching to workspace ${args[0]}...`);
806
+ const ws = await switchWorkspace(tui, args[0]);
807
+ if (ws) {
808
+ return `Switched to workspace: ${colors.accentBold(ws.name)}`;
809
+ }
810
+ }
811
+ },
812
+ {
813
+ name: "create",
814
+ description: "Create a new workspace",
815
+ argHint: "name",
816
+ minArgs: 1,
817
+ requires: "auth",
818
+ run: async (ctx) => {
819
+ const { args, arbi, tui } = ctx;
820
+ const name = args.join(" ").trim();
821
+ showMessage(tui, `Creating workspace "${name}"...`);
822
+ const ws = await sdk.workspaces.createWorkspace(arbi, name);
823
+ const selected = await switchWorkspace(tui, ws.external_id);
824
+ if (selected) {
825
+ return `Created and switched to workspace: ${colors.accentBold(selected.name)}`;
826
+ }
827
+ }
828
+ },
829
+ {
830
+ name: "ws-delete",
831
+ description: "Delete a workspace",
832
+ argHint: "id",
833
+ minArgs: 1,
834
+ requires: "auth",
835
+ run: async (ctx) => {
836
+ const { args, arbi } = ctx;
837
+ await sdk.workspaces.deleteWorkspaces(arbi, [args[0]]);
838
+ return `${colors.success("Deleted")} workspace ${args[0]}`;
839
+ }
840
+ },
841
+ {
842
+ name: "ws-users",
843
+ description: "List users in current workspace",
844
+ requires: "workspace",
845
+ run: async (ctx) => {
846
+ const { arbi } = ctx;
847
+ const users = await sdk.workspaces.listWorkspaceUsers(arbi);
848
+ if (users.length === 0) return "No users in this workspace.";
849
+ const lines = users.map((u) => {
850
+ const name = sdk.formatUserName(u.user);
851
+ const nameStr = name ? colors.muted(` (${name})`) : "";
852
+ const role = u.role ? colors.muted(` [${u.role}]`) : "";
853
+ return ` ${u.user.email}${nameStr}${role}`;
854
+ });
855
+ return [`Workspace users (${users.length}):`, "", ...lines];
856
+ }
857
+ },
858
+ {
859
+ name: "ws-add-user",
860
+ description: "Add a user to current workspace",
861
+ argHint: "email",
862
+ minArgs: 1,
863
+ requires: "workspace",
864
+ run: async (ctx) => {
865
+ const { args, arbi } = ctx;
866
+ const email = args[0].trim();
867
+ await sdk.workspaces.addWorkspaceUsers(arbi, [email]);
868
+ return `${colors.success("Added")} ${email} to workspace.`;
869
+ }
870
+ },
871
+ {
872
+ name: "ws-remove-user",
873
+ description: "Remove a user from current workspace",
874
+ argHint: "email",
875
+ minArgs: 1,
876
+ requires: "workspace",
877
+ run: async (ctx) => {
878
+ const { args, arbi } = ctx;
879
+ const email = args[0].trim();
880
+ const users = await sdk.workspaces.listWorkspaceUsers(arbi);
881
+ const user = users.find((u) => u.user.email === email);
882
+ if (!user) return `User ${email} not found in this workspace.`;
883
+ const userId = user.user.external_id;
884
+ if (!userId) return `Could not resolve user ID for ${email}.`;
885
+ await sdk.workspaces.removeWorkspaceUsers(arbi, [userId]);
886
+ return `${colors.success("Removed")} ${email} from workspace.`;
887
+ }
658
888
  }
659
- }
660
- async function handleListWorkspaces(tui) {
661
- if (!requireAuth(tui, "Not authenticated.")) return;
662
- try {
663
- const wsList = await sdk.workspaces.listWorkspaces(tui.authContext.arbi);
664
- const lines = wsList.map((ws) => {
665
- const current = ws.external_id === tui.state.workspaceId ? colors.accent(" (current)") : "";
666
- return ` ${ws.external_id} ${ws.name}${current}`;
667
- });
668
- showMessage(tui, ["Workspaces:", "", ...lines].join("\n"));
669
- } catch (err) {
670
- showMessage(tui, `Failed to list workspaces: ${sdk.getErrorMessage(err)}`, "error");
889
+ ];
890
+ init_tui_helpers();
891
+ var documentCommands = [
892
+ {
893
+ name: "docs",
894
+ description: "List documents in current workspace",
895
+ requires: "workspace",
896
+ run: async (ctx) => {
897
+ const { arbi } = ctx;
898
+ const docs = await sdk.documents.listDocuments(arbi);
899
+ if (docs.length === 0) return "No documents in this workspace.";
900
+ const lines = docs.map((d) => ` ${d.external_id} ${d.file_name ?? "(unnamed)"}`);
901
+ return [`Documents (${docs.length}):`, "", ...lines];
902
+ }
903
+ },
904
+ {
905
+ name: "doc",
906
+ description: "Show document details",
907
+ argHint: "id",
908
+ minArgs: 1,
909
+ requires: "workspace",
910
+ run: async (ctx) => {
911
+ const { args, arbi } = ctx;
912
+ const docs = await sdk.documents.getDocuments(arbi, [args[0]]);
913
+ if (docs.length === 0) return `Document ${args[0]} not found.`;
914
+ const doc = docs[0];
915
+ const lines = [
916
+ `Document: ${colors.accent(doc.file_name ?? "(unnamed)")}`,
917
+ "",
918
+ ` ID: ${doc.external_id}`,
919
+ ` Status: ${doc.status ?? colors.muted("unknown")}`,
920
+ ` Size: ${doc.file_size != null ? `${doc.file_size} bytes` : colors.muted("unknown")}`,
921
+ ` Created: ${doc.created_at ?? colors.muted("unknown")}`
922
+ ];
923
+ return lines;
924
+ }
925
+ },
926
+ {
927
+ name: "upload",
928
+ description: "Upload a file",
929
+ argHint: "path",
930
+ minArgs: 1,
931
+ requires: "workspace",
932
+ run: async (ctx) => {
933
+ const { args, tui, wsContext } = ctx;
934
+ const filePath = args.join(" ").trim();
935
+ showMessage(tui, `Uploading ${filePath}...`);
936
+ const result = await sdk.documentsNode.uploadLocalFile(
937
+ {
938
+ baseUrl: wsContext.config.baseUrl,
939
+ accessToken: wsContext.accessToken,
940
+ workspaceKeyHeader: wsContext.workspaceKeyHeader
941
+ },
942
+ wsContext.workspaceId,
943
+ filePath
944
+ );
945
+ const ids = result.doc_ext_ids.join(", ");
946
+ showMessage(tui, `${colors.success("Uploaded")} ${result.fileName} \u2014 doc ID(s): ${ids}`);
947
+ if (result.duplicates && result.duplicates.length > 0) {
948
+ showMessage(tui, `Duplicates detected: ${result.duplicates.join(", ")}`, "warning");
949
+ }
950
+ }
951
+ },
952
+ {
953
+ name: "upload-url",
954
+ description: "Upload a document from URL",
955
+ argHint: "url",
956
+ minArgs: 1,
957
+ requires: "workspace",
958
+ run: async (ctx) => {
959
+ const { args, arbi, wsContext, tui } = ctx;
960
+ const url = args[0].trim();
961
+ showMessage(tui, `Uploading from ${url}...`);
962
+ const result = await sdk.documents.uploadUrl(arbi, [url], wsContext.workspaceId);
963
+ const ids = result.doc_ext_ids.join(", ");
964
+ return `${colors.success("Uploaded")} from URL \u2014 doc ID(s): ${ids}`;
965
+ }
966
+ },
967
+ {
968
+ name: "delete",
969
+ description: "Delete a document",
970
+ argHint: "doc-id",
971
+ minArgs: 1,
972
+ requires: "workspace",
973
+ run: async (ctx) => {
974
+ const { args, arbi, tui } = ctx;
975
+ const docId = args[0].trim();
976
+ showMessage(tui, `Deleting document ${docId}...`);
977
+ await sdk.documents.deleteDocuments(arbi, [docId]);
978
+ return `${colors.success("Deleted")} document ${docId}`;
979
+ }
980
+ },
981
+ {
982
+ name: "parsed",
983
+ description: "Show parsed content of a document",
984
+ argHint: "doc-id",
985
+ minArgs: 1,
986
+ requires: "workspace",
987
+ run: async (ctx) => {
988
+ const { args, authHeaders } = ctx;
989
+ const docId = args[0].trim();
990
+ const data = await sdk.documents.getParsedContent(authHeaders, docId, "content");
991
+ const content = data.content;
992
+ if (!content) return `No parsed content available for document ${docId}.`;
993
+ const maxLen = 3e3;
994
+ const truncated = content.length > maxLen ? content.slice(0, maxLen) + "\n... (truncated)" : content;
995
+ return [`Parsed content for ${docId}:`, "", truncated];
996
+ }
671
997
  }
672
- }
673
- async function handleListDocs(tui) {
674
- if (!tui.wsContext) {
675
- showMessage(tui, "No workspace selected. Use /workspace <id> first.", "warning");
676
- return;
998
+ ];
999
+ var conversationCommands = [
1000
+ {
1001
+ name: "conversations",
1002
+ description: "List conversations",
1003
+ requires: "workspace",
1004
+ run: async (ctx) => {
1005
+ const { arbi } = ctx;
1006
+ const convs = await sdk.conversations.listConversations(arbi);
1007
+ if (convs.length === 0) return "No conversations in this workspace.";
1008
+ const lines = convs.map((c) => ` ${c.external_id} ${c.title ?? "(untitled)"}`);
1009
+ return [`Conversations (${convs.length}):`, "", ...lines];
1010
+ }
1011
+ },
1012
+ {
1013
+ name: "conv-delete",
1014
+ description: "Delete a conversation",
1015
+ argHint: "id",
1016
+ minArgs: 1,
1017
+ requires: "workspace",
1018
+ run: async (ctx) => {
1019
+ const { args, arbi } = ctx;
1020
+ await sdk.conversations.deleteConversation(arbi, args[0]);
1021
+ return `${colors.success("Deleted")} conversation ${args[0]}`;
1022
+ }
1023
+ },
1024
+ {
1025
+ name: "conv-title",
1026
+ description: "Rename a conversation",
1027
+ argHint: "id title",
1028
+ minArgs: 2,
1029
+ requires: "workspace",
1030
+ run: async (ctx) => {
1031
+ const { args, arbi } = ctx;
1032
+ const [id, ...titleParts] = args;
1033
+ const title = titleParts.join(" ");
1034
+ await sdk.conversations.updateConversationTitle(arbi, id, title);
1035
+ return `Renamed conversation ${id} to: ${colors.accent(title)}`;
1036
+ }
1037
+ },
1038
+ {
1039
+ name: "conv-share",
1040
+ description: "Share a conversation",
1041
+ argHint: "id",
1042
+ minArgs: 1,
1043
+ requires: "workspace",
1044
+ run: async (ctx) => {
1045
+ const { args, arbi } = ctx;
1046
+ const result = await sdk.conversations.shareConversation(arbi, args[0]);
1047
+ const shareId = result.share_ext_id ?? result.external_id;
1048
+ return `${colors.success("Shared")} conversation ${args[0]} \u2014 share ID: ${shareId}`;
1049
+ }
677
1050
  }
678
- try {
679
- const docs = await sdk.documents.listDocuments(tui.wsContext.arbi);
680
- if (docs.length === 0) {
681
- showMessage(tui, "No documents in this workspace.");
682
- } else {
683
- const lines = docs.map((d) => ` ${d.external_id} ${d.file_name ?? "(unnamed)"}`);
684
- showMessage(tui, [`Documents (${docs.length}):`, "", ...lines].join("\n"));
1051
+ ];
1052
+ var tagCommands = [
1053
+ {
1054
+ name: "tags",
1055
+ description: "List tags in workspace",
1056
+ requires: "workspace",
1057
+ run: async (ctx) => {
1058
+ const { arbi } = ctx;
1059
+ const data = await sdk.tags.listTags(arbi);
1060
+ if (data.length === 0) return "No tags in this workspace.";
1061
+ const lines = data.map((t) => ` ${t.external_id} ${t.name}`);
1062
+ return [`Tags (${data.length}):`, "", ...lines];
1063
+ }
1064
+ },
1065
+ {
1066
+ name: "tag-create",
1067
+ description: "Create a tag",
1068
+ argHint: "name",
1069
+ minArgs: 1,
1070
+ requires: "workspace",
1071
+ run: async (ctx) => {
1072
+ const { args, arbi } = ctx;
1073
+ const name = args.join(" ").trim();
1074
+ const tag = await sdk.tags.createTag(arbi, { name });
1075
+ return `${colors.success("Created")} tag "${tag.name}" \u2014 ID: ${tag.external_id}`;
1076
+ }
1077
+ },
1078
+ {
1079
+ name: "tag-delete",
1080
+ description: "Delete a tag",
1081
+ argHint: "id",
1082
+ minArgs: 1,
1083
+ requires: "workspace",
1084
+ run: async (ctx) => {
1085
+ const { args, arbi } = ctx;
1086
+ await sdk.tags.deleteTag(arbi, args[0]);
1087
+ return `${colors.success("Deleted")} tag ${args[0]}`;
685
1088
  }
686
- } catch (err) {
687
- showMessage(tui, `Failed to list documents: ${sdk.getErrorMessage(err)}`, "error");
688
1089
  }
689
- }
690
- async function handleListContacts(tui) {
691
- if (!requireAuth(tui)) return;
692
- try {
693
- const contactList = await sdk.contacts.listContacts(tui.authContext.arbi);
694
- const { registered, pending } = sdk.contacts.groupContactsByStatus(contactList);
695
- if (registered.length === 0 && pending.length === 0) {
696
- showMessage(tui, "No contacts. Use the web app to add contacts.");
697
- } else {
1090
+ ];
1091
+ var miscCommands = [
1092
+ {
1093
+ name: "contacts",
1094
+ description: "List contacts (type @name to DM)",
1095
+ requires: "auth",
1096
+ run: async (ctx) => {
1097
+ const { arbi } = ctx;
1098
+ const contactList = await sdk.contacts.listContacts(arbi);
1099
+ const { registered, pending } = sdk.contacts.groupContactsByStatus(contactList);
1100
+ if (registered.length === 0 && pending.length === 0) {
1101
+ return "No contacts. Use the web app to add contacts.";
1102
+ }
698
1103
  const lines = [];
699
1104
  if (registered.length > 0) {
700
1105
  lines.push(`Contacts (${registered.length}):`, "");
@@ -712,184 +1117,217 @@ async function handleListContacts(tui) {
712
1117
  }
713
1118
  }
714
1119
  lines.push("", colors.muted("Type @name to start a DM with a registered contact."));
715
- showMessage(tui, lines.join("\n"));
1120
+ return lines;
1121
+ }
1122
+ },
1123
+ {
1124
+ name: "invite",
1125
+ description: "Invite a contact",
1126
+ argHint: "email",
1127
+ minArgs: 1,
1128
+ requires: "auth",
1129
+ run: async (ctx) => {
1130
+ const { args, arbi, tui } = ctx;
1131
+ const email = args[0].trim();
1132
+ const { showMessage: showMessage2 } = await Promise.resolve().then(() => (init_tui_helpers(), tui_helpers_exports));
1133
+ showMessage2(tui, `Inviting ${email}...`);
1134
+ const result = await sdk.contacts.addContacts(arbi, [email]);
1135
+ const contact = result[0];
1136
+ if (contact?.status === "registered") {
1137
+ return `${colors.success("Added")} ${email} \u2014 already registered. You can DM them with @${email.split("@")[0]}`;
1138
+ }
1139
+ return `${colors.success("Invited")} ${email} \u2014 invitation sent. They'll appear in /contacts once they register.`;
1140
+ }
1141
+ },
1142
+ {
1143
+ name: "models",
1144
+ description: "List available AI models",
1145
+ requires: "auth",
1146
+ run: async (ctx) => {
1147
+ const { arbi } = ctx;
1148
+ const data = await sdk.health.getHealthModels(arbi);
1149
+ const models = Array.isArray(data) ? data : data.data ?? [];
1150
+ if (models.length === 0) return "No models available.";
1151
+ const lines = [`Models (${models.length}):`, ""];
1152
+ for (const m of models) {
1153
+ const model = m;
1154
+ const name = model.model_name ?? model.name ?? "unknown";
1155
+ const provider = model.provider ?? "";
1156
+ const apiType = model.api_type ?? "";
1157
+ const parts = [colors.accent(name)];
1158
+ if (provider) parts.push(`provider: ${provider}`);
1159
+ if (apiType) parts.push(`api: ${apiType}`);
1160
+ lines.push(` ${parts.join(" ")}`);
1161
+ }
1162
+ return lines;
1163
+ }
1164
+ },
1165
+ {
1166
+ name: "health",
1167
+ description: "Show system health status",
1168
+ requires: "auth",
1169
+ run: async (ctx) => {
1170
+ const { arbi } = ctx;
1171
+ const data = await sdk.health.getHealth(arbi);
1172
+ const lines = ["System Health:", ""];
1173
+ const status = data.status;
1174
+ if (status) {
1175
+ const statusColor = status === "healthy" ? colors.success(status) : colors.warning(status);
1176
+ lines.push(` Status: ${statusColor}`);
1177
+ }
1178
+ const services = data.services;
1179
+ if (services) {
1180
+ lines.push("", " Services:");
1181
+ for (const [name, info] of Object.entries(services)) {
1182
+ const svc = info;
1183
+ const svcStatus = svc.status;
1184
+ const statusStr = svcStatus ? svcStatus === "healthy" ? colors.success(svcStatus) : colors.error(svcStatus) : colors.muted("unknown");
1185
+ lines.push(` ${name}: ${statusStr}`);
1186
+ }
1187
+ }
1188
+ return lines;
1189
+ }
1190
+ },
1191
+ {
1192
+ name: "settings",
1193
+ description: "Show user settings",
1194
+ requires: "auth",
1195
+ run: async (ctx) => {
1196
+ const { arbi } = ctx;
1197
+ const data = await sdk.settings.getSettings(arbi);
1198
+ const entries = Object.entries(data);
1199
+ if (entries.length === 0) return "No settings configured.";
1200
+ const lines = ["User Settings:", ""];
1201
+ for (const [key, value] of entries) {
1202
+ const display = typeof value === "object" ? JSON.stringify(value) : String(value);
1203
+ lines.push(` ${colors.accent(key)}: ${display}`);
1204
+ }
1205
+ return lines;
716
1206
  }
717
- } catch (err) {
718
- showMessage(tui, `Failed to list contacts: ${sdk.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
1207
  }
727
- try {
728
- showMessage(tui, `Inviting ${email}...`);
729
- const result = await sdk.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
- );
1208
+ ];
1209
+ var MAX_PASSAGE_CHARS = 2e3;
1210
+ var CitationPanel = class extends piTui.Box {
1211
+ constructor(citation, passageIndex = 0) {
1212
+ super(1, 1);
1213
+ const chunk = citation.chunks[passageIndex];
1214
+ const docTitle = chunk?.metadata?.doc_title ?? "Unknown document";
1215
+ const page = chunk?.metadata?.page_number;
1216
+ const headerParts = [
1217
+ `${colors.accentBold(`[Citation ${citation.citationNum}]`)} ${colors.textBold(docTitle)}`
1218
+ ];
1219
+ if (page != null) {
1220
+ headerParts.push(colors.muted(` Page ${page}`));
1221
+ }
1222
+ if (citation.chunks.length > 1) {
1223
+ headerParts.push(colors.muted(` Passage ${passageIndex + 1}/${citation.chunks.length}`));
1224
+ }
1225
+ this.addChild(new piTui.Text(headerParts.join(""), 0, 0));
1226
+ this.addChild(new piTui.Text("", 0, 0));
1227
+ this.addChild(new piTui.Text(colors.textBold("Statement:"), 0, 0));
1228
+ this.addChild(new piTui.Text(` ${citation.citationData.statement}`, 0, 0));
1229
+ this.addChild(new piTui.Text("", 0, 0));
1230
+ this.addChild(new piTui.Text(colors.textBold("Passage:"), 0, 0));
1231
+ if (chunk) {
1232
+ let content = chunk.content;
1233
+ if (content.length > MAX_PASSAGE_CHARS) {
1234
+ content = content.slice(0, MAX_PASSAGE_CHARS) + "\n...(truncated)";
1235
+ }
1236
+ this.addChild(new piTui.Markdown(content, 0, 0, markdownTheme));
736
1237
  } else {
737
- showMessage(
738
- tui,
739
- `${colors.success("Invited")} ${email} \u2014 invitation sent. They'll appear in /contacts once they register.`
1238
+ this.addChild(new piTui.Text(colors.muted(" (no passage data available)"), 0, 0));
1239
+ }
1240
+ this.addChild(new piTui.Text("", 0, 0));
1241
+ const hints = [];
1242
+ if (citation.chunks.length > 1) {
1243
+ hints.push(
1244
+ `/cite ${citation.citationNum} <1-${citation.chunks.length}> to view other passages`
740
1245
  );
741
1246
  }
742
- } catch (err) {
743
- showMessage(tui, `Failed to invite contact: ${sdk.getErrorMessage(err)}`, "error");
1247
+ hints.push("Press Escape to close");
1248
+ this.addChild(new piTui.Text(colors.muted(hints.join(" \xB7 ")), 0, 0));
744
1249
  }
745
- }
746
- async function handleListConversations(tui) {
747
- if (!tui.wsContext) {
748
- showMessage(tui, "No workspace selected. Use /workspace <id> first.", "warning");
749
- return;
750
- }
751
- try {
752
- const convs = await sdk.conversations.listConversations(tui.wsContext.arbi);
753
- if (convs.length === 0) {
754
- showMessage(tui, "No conversations in this workspace.");
755
- } else {
756
- const lines = convs.map((c) => ` ${c.external_id} ${c.title ?? "(untitled)"}`);
757
- showMessage(tui, [`Conversations (${convs.length}):`, "", ...lines].join("\n"));
1250
+ };
1251
+
1252
+ // src/commands/citation.ts
1253
+ function handleCite(ctx) {
1254
+ const { tui, args } = ctx;
1255
+ if (!tui.lastMetadata) {
1256
+ return "No citation data available. Ask a question first.";
1257
+ }
1258
+ const count = sdk.countCitations(tui.lastMetadata);
1259
+ if (count === 0) {
1260
+ return "The last response contained no citations.";
1261
+ }
1262
+ const resolved = sdk.resolveCitations(tui.lastMetadata);
1263
+ if (args.length >= 1) {
1264
+ const citationNum = args[0];
1265
+ const citation = resolved.find((r) => r.citationNum === citationNum);
1266
+ if (!citation) {
1267
+ return `Citation [${citationNum}] not found. Use /cite to list all citations.`;
758
1268
  }
759
- } catch (err) {
760
- showMessage(tui, `Failed to list conversations: ${sdk.getErrorMessage(err)}`, "error");
761
- }
762
- }
763
- function handleNewConversation(tui) {
764
- tui.state.conversationMessageId = null;
765
- tui.store.clearChatSession();
766
- showMessage(tui, "Started new conversation.");
767
- }
768
- function handleStatus(tui) {
769
- const { state } = tui;
770
- const lines = [
771
- `Authenticated: ${state.isAuthenticated ? colors.success("yes") : colors.error("no")}`,
772
- `Workspace: ${state.workspaceName ? colors.accent(state.workspaceName) : colors.muted("none")}`,
773
- `Workspace ID: ${state.workspaceId ?? colors.muted("none")}`,
774
- `Conversation: ${state.conversationMessageId ? colors.muted(state.conversationMessageId) : colors.muted("new")}`,
775
- `Status: ${state.activityStatus}`,
776
- `WebSocket: ${tui.wsConnected ? colors.success("connected") : colors.muted("disconnected")}`
777
- ];
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 sdk.documentsNode.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");
1269
+ let passageIndex = 0;
1270
+ if (args.length >= 2) {
1271
+ const p = parseInt(args[1], 10);
1272
+ if (isNaN(p) || p < 1 || p > citation.chunks.length) {
1273
+ return `Passage must be between 1 and ${citation.chunks.length}.`;
1274
+ }
1275
+ passageIndex = p - 1;
805
1276
  }
806
- } catch (err) {
807
- showMessage(tui, `Failed to upload: ${sdk.getErrorMessage(err)}`, "error");
808
- }
809
- }
810
- async function handleDelete(tui, docId) {
811
- if (!tui.wsContext) {
812
- showMessage(tui, "No workspace selected. Use /workspace <id> first.", "warning");
813
- return;
814
- }
815
- if (!docId?.trim()) {
816
- showMessage(tui, "Usage: /delete <doc-id> (use /docs to list document IDs)", "warning");
1277
+ const panel = new CitationPanel(citation, passageIndex);
1278
+ tui.showCitationOverlay(panel);
817
1279
  return;
818
1280
  }
819
- try {
820
- showMessage(tui, `Deleting document ${docId}...`);
821
- await sdk.documents.deleteDocuments(tui.wsContext.arbi, [docId.trim()]);
822
- showMessage(tui, `${colors.success("Deleted")} document ${docId}`);
823
- } catch (err) {
824
- showMessage(tui, `Failed to delete document: ${sdk.getErrorMessage(err)}`, "error");
1281
+ const summaries = sdk.summarizeCitations(resolved);
1282
+ const lines = [`Citations (${summaries.length}):
1283
+ `];
1284
+ for (const s of summaries) {
1285
+ const page = s.pageNumber != null ? `, p${s.pageNumber}` : "";
1286
+ const chunks = s.chunkCount > 1 ? ` (${s.chunkCount} passages)` : "";
1287
+ const truncated = s.statement.length > 100 ? s.statement.slice(0, 100).replace(/\n/g, " ").trim() + "..." : s.statement.replace(/\n/g, " ").trim();
1288
+ lines.push(
1289
+ ` ${colors.accent(`[${s.citationNum}]`)} ${colors.textBold(s.docTitle)}${page}${chunks}`
1290
+ );
1291
+ lines.push(` ${colors.muted(truncated)}`);
825
1292
  }
1293
+ lines.push(colors.muted("\nUse /cite <N> to view full passage."));
1294
+ return lines;
826
1295
  }
827
- function handleLogout(tui) {
828
- tui.store.deleteCredentials();
829
- tui.store.updateConfig({ selectedWorkspaceId: void 0 });
830
- tui.store.clearChatSession();
831
- tui.authContext = null;
832
- tui.state.isAuthenticated = false;
833
- tui.state.workspaceId = null;
834
- tui.state.workspaceName = null;
835
- tui.state.conversationMessageId = null;
836
- showMessage(tui, "Logged out. Use /login to authenticate again.");
837
- }
838
- async function handleHealth(tui) {
839
- if (!requireAuth(tui)) return;
840
- try {
841
- const data = await sdk.health.getHealth(tui.authContext.arbi);
842
- const lines = ["System Health:", ""];
843
- const status = data.status;
844
- if (status) {
845
- const statusColor = status === "healthy" ? colors.success(status) : colors.warning(status);
846
- lines.push(` Status: ${statusColor}`);
847
- }
848
- const services = data.services;
849
- if (services) {
850
- lines.push("", " Services:");
851
- for (const [name, info] of Object.entries(services)) {
852
- const svc = info;
853
- const svcStatus = svc.status;
854
- const statusStr = svcStatus ? svcStatus === "healthy" ? colors.success(svcStatus) : colors.error(svcStatus) : colors.muted("unknown");
855
- lines.push(` ${name}: ${statusStr}`);
856
- }
857
- }
858
- showMessage(tui, lines.join("\n"));
859
- } catch (err) {
860
- showMessage(tui, `Failed to fetch health status: ${sdk.getErrorMessage(err)}`, "error");
861
- }
862
- }
863
- async function handleModels(tui) {
864
- if (!requireAuth(tui)) return;
865
- try {
866
- const data = await sdk.health.getHealthModels(tui.authContext.arbi);
867
- const models = Array.isArray(data) ? data : data.data ?? [];
868
- if (models.length === 0) {
869
- showMessage(tui, "No models available.");
870
- return;
871
- }
872
- const lines = [`Models (${models.length}):`, ""];
873
- for (const m of models) {
874
- const model = m;
875
- const name = model.model_name ?? model.name ?? "unknown";
876
- const provider = model.provider ?? "";
877
- const apiType = model.api_type ?? "";
878
- const parts = [colors.accent(name)];
879
- if (provider) parts.push(`provider: ${provider}`);
880
- if (apiType) parts.push(`api: ${apiType}`);
881
- lines.push(` ${parts.join(" ")}`);
882
- }
883
- showMessage(tui, lines.join("\n"));
884
- } catch (err) {
885
- showMessage(tui, `Failed to fetch models: ${sdk.getErrorMessage(err)}`, "error");
1296
+ var citationCommands = [
1297
+ {
1298
+ name: "cite",
1299
+ description: "Browse citations from last response",
1300
+ requires: "none",
1301
+ argHint: "[N] [passage]",
1302
+ run: (ctx) => handleCite(ctx)
1303
+ },
1304
+ {
1305
+ name: "refs",
1306
+ description: "Browse citations from last response",
1307
+ requires: "none",
1308
+ hidden: true,
1309
+ run: (ctx) => handleCite(ctx)
886
1310
  }
1311
+ ];
1312
+
1313
+ // src/commands/index.ts
1314
+ function registerAllCommands() {
1315
+ registerCommands(generalCommands);
1316
+ registerCommands(authCommands);
1317
+ registerCommands(workspaceCommands);
1318
+ registerCommands(documentCommands);
1319
+ registerCommands(conversationCommands);
1320
+ registerCommands(tagCommands);
1321
+ registerCommands(miscCommands);
1322
+ registerCommands(citationCommands);
887
1323
  }
888
1324
  async function streamResponse(tui, response) {
889
1325
  tui.chatLog.startAssistant();
890
1326
  tui.state.activityStatus = "streaming";
891
1327
  tui.requestRender();
892
1328
  let accumulated = "";
1329
+ let elapsedTime = null;
1330
+ let firstToken = true;
893
1331
  const callbacks = {
894
1332
  onStreamStart: (data) => {
895
1333
  if (data.assistant_message_ext_id) {
@@ -897,6 +1335,10 @@ async function streamResponse(tui, response) {
897
1335
  }
898
1336
  },
899
1337
  onToken: (content) => {
1338
+ if (firstToken) {
1339
+ firstToken = false;
1340
+ tui.chatLog.clearActiveSteps();
1341
+ }
900
1342
  accumulated += content;
901
1343
  tui.chatLog.updateAssistant(accumulated);
902
1344
  tui.requestRender();
@@ -908,6 +1350,9 @@ async function streamResponse(tui, response) {
908
1350
  tui.requestRender();
909
1351
  }
910
1352
  },
1353
+ onElapsedTime: (t) => {
1354
+ elapsedTime = t;
1355
+ },
911
1356
  onError: (message) => {
912
1357
  tui.chatLog.addSystem(`Stream error: ${message}`, "error");
913
1358
  tui.requestRender();
@@ -916,6 +1361,13 @@ async function streamResponse(tui, response) {
916
1361
  try {
917
1362
  const result = await sdk.streamSSE(response, callbacks);
918
1363
  tui.chatLog.finalizeAssistant();
1364
+ tui.lastMetadata = result.metadata ?? null;
1365
+ const summary = sdk.formatStreamSummary(result, elapsedTime);
1366
+ if (summary) {
1367
+ const refs = sdk.countCitations(result.metadata ?? null);
1368
+ const refSuffix = refs > 0 ? ` \xB7 ${refs} ref${refs === 1 ? "" : "s"}` : "";
1369
+ tui.chatLog.addSummary(`[${summary}${refSuffix}]`);
1370
+ }
919
1371
  tui.state.activityStatus = "idle";
920
1372
  tui.requestRender();
921
1373
  return result;
@@ -927,6 +1379,7 @@ async function streamResponse(tui, response) {
927
1379
  throw err;
928
1380
  }
929
1381
  }
1382
+ init_tui_helpers();
930
1383
  function deriveEncryptionKeypair(signingPrivateKeyBase64) {
931
1384
  const signingPrivateKey = client.base64ToBytes(signingPrivateKeyBase64);
932
1385
  const ed25519PublicKey = signingPrivateKey.slice(32, 64);
@@ -1133,7 +1586,7 @@ async function connectTuiWebSocket(options) {
1133
1586
  baseUrl,
1134
1587
  accessToken,
1135
1588
  onMessage: (msg) => handleMessage(msg, toasts, tui ?? null),
1136
- onClose: (_code, _reason) => {
1589
+ onClose: () => {
1137
1590
  toasts.show("WebSocket disconnected", "warning", DURATION_DEFAULT);
1138
1591
  },
1139
1592
  onReconnecting: (attempt, maxRetries) => {
@@ -1177,6 +1630,12 @@ var ArbiTui = class {
1177
1630
  encryptionKeyPair = null;
1178
1631
  /** Current DM channel (null = AI chat mode). */
1179
1632
  dmChannel = null;
1633
+ /** Last response metadata — used by /cite for citation browsing. */
1634
+ lastMetadata = null;
1635
+ /** Active citation overlay handle (null when not showing). */
1636
+ citationOverlay = null;
1637
+ /** Input listener ID for overlay dismiss (null when no overlay). */
1638
+ overlayInputListener = null;
1180
1639
  constructor(store) {
1181
1640
  this.store = store;
1182
1641
  this.state = {
@@ -1186,6 +1645,7 @@ var ArbiTui = class {
1186
1645
  activityStatus: "idle",
1187
1646
  isAuthenticated: false
1188
1647
  };
1648
+ registerAllCommands();
1189
1649
  this.tui = new piTui.TUI(new piTui.ProcessTerminal());
1190
1650
  this.header = new piTui.Text(formatHeader(null, "starting..."), 1, 0);
1191
1651
  this.chatLog = new ChatLog();
@@ -1200,11 +1660,11 @@ var ArbiTui = class {
1200
1660
  this.tui.setFocus(this.editor);
1201
1661
  }
1202
1662
  // ── Lifecycle ──────────────────────────────────────────────────────────
1203
- /** Start the TUI event loop. */
1663
+ /** Start the TUI event loop (clears screen for fullscreen layout). */
1204
1664
  start() {
1205
1665
  this.tui.start();
1206
1666
  this.updateHeader();
1207
- this.tui.requestRender();
1667
+ this.tui.requestRender(true);
1208
1668
  }
1209
1669
  /** Gracefully shut down the TUI and exit. */
1210
1670
  shutdown() {
@@ -1237,12 +1697,12 @@ var ArbiTui = class {
1237
1697
  this.tui.setFocus(this.editor);
1238
1698
  this.tui.start();
1239
1699
  this.updateHeader();
1240
- this.tui.requestRender();
1700
+ this.tui.requestRender(true);
1241
1701
  }
1242
1702
  /** Create and wire a new editor instance. */
1243
1703
  createEditor() {
1244
1704
  const editor = new ArbiEditor(this.tui);
1245
- editor.setAutocompleteProvider(new piTui.CombinedAutocompleteProvider(commands));
1705
+ editor.setAutocompleteProvider(new piTui.CombinedAutocompleteProvider(toSlashCommands()));
1246
1706
  editor.onSubmit = (text) => this.handleSubmit(text);
1247
1707
  editor.onEscape = () => this.handleAbort();
1248
1708
  editor.onCtrlC = () => this.shutdown();
@@ -1312,7 +1772,7 @@ var ArbiTui = class {
1312
1772
  return;
1313
1773
  }
1314
1774
  if (trimmed.startsWith("/")) {
1315
- const handled = await handleCommand(this, trimmed);
1775
+ const handled = await dispatchCommand(this, trimmed);
1316
1776
  if (handled) return;
1317
1777
  }
1318
1778
  if (this.dmChannel) {
@@ -1385,6 +1845,37 @@ var ArbiTui = class {
1385
1845
  this.header.setText(formatHeader(this.state.workspaceName, statusText));
1386
1846
  }
1387
1847
  }
1848
+ // ── Citation overlay ───────────────────────────────────────────────────
1849
+ /** Show a component as a centered overlay (used by /cite). */
1850
+ showCitationOverlay(component) {
1851
+ this.dismissCitationOverlay();
1852
+ this.citationOverlay = this.tui.showOverlay(component, {
1853
+ anchor: "center",
1854
+ width: "80%",
1855
+ maxHeight: "80%",
1856
+ margin: 2
1857
+ });
1858
+ this.overlayInputListener = this.tui.addInputListener((data) => {
1859
+ if (piTui.matchesKey(data, piTui.Key.escape)) {
1860
+ this.dismissCitationOverlay();
1861
+ return { consume: true };
1862
+ }
1863
+ return void 0;
1864
+ });
1865
+ this.tui.requestRender();
1866
+ }
1867
+ /** Dismiss the active citation overlay. */
1868
+ dismissCitationOverlay() {
1869
+ if (this.citationOverlay) {
1870
+ this.citationOverlay.hide();
1871
+ this.citationOverlay = null;
1872
+ }
1873
+ if (this.overlayInputListener) {
1874
+ this.overlayInputListener();
1875
+ this.overlayInputListener = null;
1876
+ }
1877
+ this.tui.requestRender();
1878
+ }
1388
1879
  // ── DM channel accessors (used by dm-handler) ────────────────────────────
1389
1880
  /** Current DM channel (null = AI chat mode). */
1390
1881
  get currentDmChannel() {