@arbidocs/tui 0.3.66 → 0.3.68
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 +736 -36
- package/dist/index.js.map +1 -1
- package/package.json +7 -5
package/dist/index.js
CHANGED
|
@@ -8,6 +8,10 @@ var chalk = require('chalk');
|
|
|
8
8
|
require('fake-indexeddb/auto');
|
|
9
9
|
var client = require('@arbidocs/client');
|
|
10
10
|
var prompts = require('@inquirer/prompts');
|
|
11
|
+
var child_process = require('child_process');
|
|
12
|
+
var fs = require('fs');
|
|
13
|
+
var os = require('os');
|
|
14
|
+
var path = require('path');
|
|
11
15
|
|
|
12
16
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
17
|
|
|
@@ -213,18 +217,39 @@ var RightAlignedText = class {
|
|
|
213
217
|
return [" ".repeat(padding) + styled];
|
|
214
218
|
}
|
|
215
219
|
};
|
|
220
|
+
var GLYPH_LIFECYCLE = ">";
|
|
221
|
+
var GLYPH_TOOL = "\u25B8";
|
|
222
|
+
var GLYPH_RESULT = "\u2192";
|
|
216
223
|
var AgentStep = class extends piTui.Text {
|
|
217
224
|
completed = false;
|
|
218
|
-
constructor(
|
|
219
|
-
|
|
220
|
-
super(styled, 2, 0);
|
|
225
|
+
constructor(label, details) {
|
|
226
|
+
super(buildText(label, details), 2, 0);
|
|
221
227
|
}
|
|
222
|
-
/** Mark this step as completed.
|
|
228
|
+
/** Mark this step as completed. Renderer is unchanged for now —
|
|
229
|
+
* see the file-header comment for the planned focusable-collapse
|
|
230
|
+
* upgrade path. */
|
|
223
231
|
complete() {
|
|
224
232
|
if (this.completed) return;
|
|
225
233
|
this.completed = true;
|
|
226
234
|
}
|
|
227
235
|
};
|
|
236
|
+
function buildText(label, details) {
|
|
237
|
+
const hasDetails = !!(details?.tool || details?.args || details?.result);
|
|
238
|
+
const glyph = hasDetails ? GLYPH_TOOL : GLYPH_LIFECYCLE;
|
|
239
|
+
const header = `${colors.stepPending(glyph)} ${hasDetails ? colors.accent(label) : colors.muted(label)}`;
|
|
240
|
+
if (!hasDetails) return header;
|
|
241
|
+
const lines = [header];
|
|
242
|
+
if (details?.tool && details.tool !== label) {
|
|
243
|
+
lines.push(` ${colors.muted("tool:")} ${details.tool}`);
|
|
244
|
+
}
|
|
245
|
+
if (details?.args) {
|
|
246
|
+
lines.push(` ${colors.muted("args:")} ${details.args}`);
|
|
247
|
+
}
|
|
248
|
+
if (details?.result) {
|
|
249
|
+
lines.push(` ${colors.muted(GLYPH_RESULT)} ${details.result}`);
|
|
250
|
+
}
|
|
251
|
+
return lines.join("\n");
|
|
252
|
+
}
|
|
228
253
|
|
|
229
254
|
// src/components/chat-log.ts
|
|
230
255
|
var ChatLog = class extends piTui.Container {
|
|
@@ -282,10 +307,14 @@ var ChatLog = class extends piTui.Container {
|
|
|
282
307
|
updateAssistant(text) {
|
|
283
308
|
this.activeAssistant?.setText(text);
|
|
284
309
|
}
|
|
285
|
-
/** Add an agent step to the current streaming run.
|
|
286
|
-
|
|
310
|
+
/** Add an agent step to the current streaming run. Optional
|
|
311
|
+
* ``details`` enriches the rendered block with ``tool`` / ``args``
|
|
312
|
+
* / ``result`` lines beneath the header — claude-code-style
|
|
313
|
+
* tool-use expansion. Callers that don't have structured detail
|
|
314
|
+
* can omit the second arg and get the original flat behaviour. */
|
|
315
|
+
addAgentStep(label, details) {
|
|
287
316
|
if (!this.activeAssistant) return;
|
|
288
|
-
const step = new AgentStep(
|
|
317
|
+
const step = new AgentStep(label, details);
|
|
289
318
|
this.activeSteps.push(step);
|
|
290
319
|
this.addChild(step);
|
|
291
320
|
}
|
|
@@ -303,6 +332,107 @@ var ChatLog = class extends piTui.Container {
|
|
|
303
332
|
this.addChild(new piTui.Spacer(1));
|
|
304
333
|
}
|
|
305
334
|
};
|
|
335
|
+
|
|
336
|
+
// src/event-log.ts
|
|
337
|
+
var WsEventLog = class {
|
|
338
|
+
entries = [];
|
|
339
|
+
capacity;
|
|
340
|
+
/** ``capacity`` defaults to 200, the same order-of-magnitude as
|
|
341
|
+
* a typical long chat session. Old entries roll off when the
|
|
342
|
+
* buffer fills — same shape as openclaw's session log. */
|
|
343
|
+
constructor(capacity = 200) {
|
|
344
|
+
this.capacity = capacity;
|
|
345
|
+
}
|
|
346
|
+
/** Append a new entry. Drops the oldest if at capacity. */
|
|
347
|
+
add(entry) {
|
|
348
|
+
this.entries.push({
|
|
349
|
+
at: entry.at ?? /* @__PURE__ */ new Date(),
|
|
350
|
+
text: entry.text,
|
|
351
|
+
level: entry.level,
|
|
352
|
+
msgType: entry.msgType
|
|
353
|
+
});
|
|
354
|
+
while (this.entries.length > this.capacity) this.entries.shift();
|
|
355
|
+
}
|
|
356
|
+
/** Read the last ``limit`` entries (most recent last). Optionally
|
|
357
|
+
* filter by level — useful for ``/events --errors``. */
|
|
358
|
+
getAll(options) {
|
|
359
|
+
let view = this.entries;
|
|
360
|
+
if (options?.level) view = view.filter((e) => e.level === options.level);
|
|
361
|
+
if (options?.limit != null) view = view.slice(-options.limit);
|
|
362
|
+
return view;
|
|
363
|
+
}
|
|
364
|
+
/** Number of entries currently retained (after eviction). */
|
|
365
|
+
get size() {
|
|
366
|
+
return this.entries.length;
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
function formatEventLine(entry) {
|
|
370
|
+
const h = String(entry.at.getHours()).padStart(2, "0");
|
|
371
|
+
const m = String(entry.at.getMinutes()).padStart(2, "0");
|
|
372
|
+
const s = String(entry.at.getSeconds()).padStart(2, "0");
|
|
373
|
+
const tag = entry.level.padEnd(7);
|
|
374
|
+
return `[${h}:${m}:${s}] ${tag} ${entry.text}`;
|
|
375
|
+
}
|
|
376
|
+
var SourcesIndex = class {
|
|
377
|
+
// Keyed by docId so successive turns add to the same entry rather
|
|
378
|
+
// than duplicating it.
|
|
379
|
+
entries = /* @__PURE__ */ new Map();
|
|
380
|
+
/** Fold every citation in ``metadata`` into the index. Idempotent
|
|
381
|
+
* on the same payload — adding the same metadata twice doesn't
|
|
382
|
+
* inflate counts. */
|
|
383
|
+
recordCitations(metadata) {
|
|
384
|
+
if (!metadata) return;
|
|
385
|
+
this.recordResolved(sdk.resolveCitations(metadata));
|
|
386
|
+
}
|
|
387
|
+
/** The underlying fold step, separated so tests can drive it with
|
|
388
|
+
* hand-rolled ``ResolvedCitation`` fixtures rather than mocking
|
|
389
|
+
* the full ``MessageMetadataPayload.tools.*`` graph that
|
|
390
|
+
* ``resolveCitations`` traverses. */
|
|
391
|
+
recordResolved(resolved) {
|
|
392
|
+
for (const c of resolved) {
|
|
393
|
+
for (const chunk of c.chunks) {
|
|
394
|
+
const docId = chunk.metadata?.doc_ext_id;
|
|
395
|
+
if (!docId) continue;
|
|
396
|
+
const title = chunk.metadata?.doc_title ?? docId;
|
|
397
|
+
const page = chunk.metadata?.page_number;
|
|
398
|
+
const existing = this.entries.get(docId);
|
|
399
|
+
if (existing) {
|
|
400
|
+
if (typeof page === "number" && !existing.pages.includes(page)) {
|
|
401
|
+
existing.pages.push(page);
|
|
402
|
+
existing.pages.sort((a, b) => a - b);
|
|
403
|
+
}
|
|
404
|
+
if (!existing.citationNums.includes(c.citationNum)) {
|
|
405
|
+
existing.citationNums.push(c.citationNum);
|
|
406
|
+
existing.citationNums.sort();
|
|
407
|
+
}
|
|
408
|
+
if (existing.title === docId && title !== docId) existing.title = title;
|
|
409
|
+
} else {
|
|
410
|
+
this.entries.set(docId, {
|
|
411
|
+
docId,
|
|
412
|
+
title,
|
|
413
|
+
pages: typeof page === "number" ? [page] : [],
|
|
414
|
+
citationNums: [c.citationNum]
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/** All entries in registration order — first-cited first. The
|
|
421
|
+
* iterator order of ``Map`` is insertion order, which is what
|
|
422
|
+
* we want for ``/sources`` (oldest at top, newest at bottom). */
|
|
423
|
+
list() {
|
|
424
|
+
return Array.from(this.entries.values());
|
|
425
|
+
}
|
|
426
|
+
/** Clear the index. Wired to ``/new`` so a fresh conversation
|
|
427
|
+
* starts with no inherited sources. */
|
|
428
|
+
clear() {
|
|
429
|
+
this.entries.clear();
|
|
430
|
+
}
|
|
431
|
+
/** Number of unique documents tracked. */
|
|
432
|
+
get size() {
|
|
433
|
+
return this.entries.size;
|
|
434
|
+
}
|
|
435
|
+
};
|
|
306
436
|
var ArbiEditor = class extends piTui.Editor {
|
|
307
437
|
/** Callback when Escape is pressed (abort streaming). */
|
|
308
438
|
onEscape;
|
|
@@ -314,8 +444,31 @@ var ArbiEditor = class extends piTui.Editor {
|
|
|
314
444
|
onCtrlW;
|
|
315
445
|
/** Callback when Ctrl+N is pressed (new conversation). */
|
|
316
446
|
onCtrlN;
|
|
447
|
+
/** Callback when Alt+1..9 is pressed (jump to citation N from
|
|
448
|
+
* the last response). The handler gets the integer ``1`` through
|
|
449
|
+
* ``9``; it is responsible for checking whether that citation
|
|
450
|
+
* exists and surfacing a useful message if not. */
|
|
451
|
+
onCitationKey;
|
|
452
|
+
/**
|
|
453
|
+
* Callback fired after every keystroke that mutates the buffer.
|
|
454
|
+
*
|
|
455
|
+
* The TUI uses this to power the **pre-flight skill hint**: when
|
|
456
|
+
* the user types ``/<slug>`` we surface the skill's description in
|
|
457
|
+
* the chat log so they see what they're about to invoke before
|
|
458
|
+
* pressing Enter. The handler is responsible for its own
|
|
459
|
+
* deduplication (e.g. only show the hint when the parsed slug
|
|
460
|
+
* changes), since this callback fires on *every* mutating key.
|
|
461
|
+
*
|
|
462
|
+
* Receives the buffer text *after* the base ``handleInput`` has
|
|
463
|
+
* applied the change. Read-only — handlers must not mutate the
|
|
464
|
+
* editor synchronously or it'll loop.
|
|
465
|
+
*/
|
|
466
|
+
onBufferChange;
|
|
317
467
|
/** Track Ctrl+C presses for double-tap exit. */
|
|
318
468
|
lastCtrlCTime = 0;
|
|
469
|
+
/** Snapshot of the last text we emitted, so we don't fire on
|
|
470
|
+
* non-mutating keys (cursor moves, redraws). */
|
|
471
|
+
lastEmittedText = "";
|
|
319
472
|
constructor(tui) {
|
|
320
473
|
super(tui, editorTheme, { paddingX: 1 });
|
|
321
474
|
}
|
|
@@ -349,7 +502,25 @@ var ArbiEditor = class extends piTui.Editor {
|
|
|
349
502
|
this.onCtrlN?.();
|
|
350
503
|
return;
|
|
351
504
|
}
|
|
505
|
+
if (this.onCitationKey) {
|
|
506
|
+
for (let n = 1; n <= 9; n++) {
|
|
507
|
+
if (piTui.matchesKey(data, piTui.Key.alt(String(n)))) {
|
|
508
|
+
this.onCitationKey(n);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
352
513
|
super.handleInput(data);
|
|
514
|
+
if (this.onBufferChange) {
|
|
515
|
+
const current = this.getText();
|
|
516
|
+
if (current !== this.lastEmittedText) {
|
|
517
|
+
this.lastEmittedText = current;
|
|
518
|
+
try {
|
|
519
|
+
this.onBufferChange(current);
|
|
520
|
+
} catch {
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
353
524
|
}
|
|
354
525
|
};
|
|
355
526
|
var LEVEL_STYLE = {
|
|
@@ -407,6 +578,7 @@ var ToastContainer = class extends piTui.Container {
|
|
|
407
578
|
// src/command-registry.ts
|
|
408
579
|
init_tui_helpers();
|
|
409
580
|
var registry = /* @__PURE__ */ new Map();
|
|
581
|
+
var KNOWN_SERVER_COMMANDS = /* @__PURE__ */ new Set(["pa", "setup", "cold-start"]);
|
|
410
582
|
function registerCommand(def) {
|
|
411
583
|
registry.set(def.name, def);
|
|
412
584
|
}
|
|
@@ -423,6 +595,15 @@ function toSlashCommands() {
|
|
|
423
595
|
}
|
|
424
596
|
return cmds;
|
|
425
597
|
}
|
|
598
|
+
function toSlashCommandsWithSkills(skills) {
|
|
599
|
+
const out = toSlashCommands();
|
|
600
|
+
const taken = new Set(out.map((c) => c.name));
|
|
601
|
+
for (const s of skills) {
|
|
602
|
+
if (taken.has(s.slug)) continue;
|
|
603
|
+
out.push({ name: s.slug, description: s.description });
|
|
604
|
+
}
|
|
605
|
+
return out;
|
|
606
|
+
}
|
|
426
607
|
function formatHelpText() {
|
|
427
608
|
const lines = [];
|
|
428
609
|
for (const def of registry.values()) {
|
|
@@ -440,21 +621,36 @@ function formatHelpText() {
|
|
|
440
621
|
" @arbi \u2014 Switch back to AI chat",
|
|
441
622
|
"",
|
|
442
623
|
"Keyboard shortcuts:",
|
|
443
|
-
" Ctrl+N
|
|
444
|
-
" Ctrl+W
|
|
445
|
-
" Ctrl+D
|
|
446
|
-
" Escape
|
|
624
|
+
" Ctrl+N \u2014 New conversation",
|
|
625
|
+
" Ctrl+W \u2014 Switch workspace",
|
|
626
|
+
" Ctrl+D \u2014 Exit",
|
|
627
|
+
" Escape \u2014 Abort streaming",
|
|
628
|
+
" Alt+1..9 \u2014 Jump to citation N from the last response",
|
|
629
|
+
"",
|
|
630
|
+
"Skills: any workspace SKILL.md with `user-invocable: true` becomes a",
|
|
631
|
+
" ``/<slug>`` command \u2014 type it like a regular slash command and the",
|
|
632
|
+
" agent handles it. ``/skills`` lists what is available."
|
|
447
633
|
].join("\n");
|
|
448
634
|
}
|
|
449
635
|
async function dispatchCommand(tui, input2) {
|
|
636
|
+
const parsed = sdk.parseSlashCommand(input2);
|
|
637
|
+
if (!parsed) return false;
|
|
638
|
+
const cmdName = parsed.slug;
|
|
639
|
+
const args = parsed.args.length > 0 ? parsed.args.split(/\s+/) : [];
|
|
450
640
|
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
641
|
const def = registry.get(cmdName);
|
|
457
642
|
if (!def) {
|
|
643
|
+
const skill = tui.skillsCache?.find(
|
|
644
|
+
(s) => s.slug === cmdName || s.name.toLowerCase() === cmdName
|
|
645
|
+
);
|
|
646
|
+
if (skill) {
|
|
647
|
+
const meta = skill.arg_hint ? `Skill: ${skill.name} ${skill.arg_hint} \u2014 ${skill.description}` : `Skill: ${skill.name} \u2014 ${skill.description}`;
|
|
648
|
+
showMessage(tui, meta, "info");
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
if (KNOWN_SERVER_COMMANDS.has(cmdName)) {
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
458
654
|
showMessage(tui, `Unknown command: /${cmdName}. Type /help for available commands.`, "warning");
|
|
459
655
|
return true;
|
|
460
656
|
}
|
|
@@ -548,6 +744,7 @@ var generalCommands = [
|
|
|
548
744
|
const { tui } = ctx;
|
|
549
745
|
tui.state.conversationMessageId = null;
|
|
550
746
|
tui.lastMetadata = null;
|
|
747
|
+
tui.sourcesIndex.clear();
|
|
551
748
|
tui.store.clearChatSession();
|
|
552
749
|
return "Started new conversation.";
|
|
553
750
|
}
|
|
@@ -1336,6 +1533,387 @@ var citationCommands = [
|
|
|
1336
1533
|
}
|
|
1337
1534
|
];
|
|
1338
1535
|
|
|
1536
|
+
// src/commands/events.ts
|
|
1537
|
+
var DEFAULT_LIMIT = 20;
|
|
1538
|
+
function parseEventsArg(arg) {
|
|
1539
|
+
if (!arg) return { limit: DEFAULT_LIMIT };
|
|
1540
|
+
const lower = arg.trim().toLowerCase();
|
|
1541
|
+
if (lower === "all") return {};
|
|
1542
|
+
if (lower === "errors" || lower === "error") {
|
|
1543
|
+
return { limit: DEFAULT_LIMIT, level: "error" };
|
|
1544
|
+
}
|
|
1545
|
+
if (lower === "warnings" || lower === "warning") {
|
|
1546
|
+
return { limit: DEFAULT_LIMIT, level: "warning" };
|
|
1547
|
+
}
|
|
1548
|
+
const n = Number.parseInt(lower, 10);
|
|
1549
|
+
if (Number.isFinite(n) && n > 0) return { limit: n };
|
|
1550
|
+
return { limit: DEFAULT_LIMIT };
|
|
1551
|
+
}
|
|
1552
|
+
var eventCommands = [
|
|
1553
|
+
{
|
|
1554
|
+
name: "events",
|
|
1555
|
+
description: "Show recent WebSocket events (replaces toasts that scroll off)",
|
|
1556
|
+
argHint: "count|all|errors",
|
|
1557
|
+
requires: "none",
|
|
1558
|
+
run: (ctx) => {
|
|
1559
|
+
const { tui, args } = ctx;
|
|
1560
|
+
const query = parseEventsArg(args[0]);
|
|
1561
|
+
const entries = tui.eventLog.getAll(query);
|
|
1562
|
+
if (entries.length === 0) {
|
|
1563
|
+
if (query.level) {
|
|
1564
|
+
return `No ${query.level} events recorded yet.`;
|
|
1565
|
+
}
|
|
1566
|
+
return "No WebSocket events recorded yet.";
|
|
1567
|
+
}
|
|
1568
|
+
const lines = entries.map((e) => {
|
|
1569
|
+
const line = formatEventLine(e);
|
|
1570
|
+
switch (e.level) {
|
|
1571
|
+
case "error":
|
|
1572
|
+
return colors.error(line);
|
|
1573
|
+
case "warning":
|
|
1574
|
+
return colors.warning(line);
|
|
1575
|
+
case "success":
|
|
1576
|
+
return colors.success(line);
|
|
1577
|
+
default:
|
|
1578
|
+
return colors.muted(line);
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
const header = query.level != null ? `Events (${entries.length}, level=${query.level}):` : `Events (${entries.length} of ${tui.eventLog.size} retained):`;
|
|
1582
|
+
return [header, "", ...lines];
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
];
|
|
1586
|
+
var MAX_CHARS_PER_PAGE = 2e3;
|
|
1587
|
+
function splitIntoPages(content) {
|
|
1588
|
+
if (!content) return [];
|
|
1589
|
+
const trimmed = content.trim();
|
|
1590
|
+
if (!trimmed) return [];
|
|
1591
|
+
if (trimmed.includes("\n---\n")) {
|
|
1592
|
+
return trimmed.split(/\n---\n/).map((p) => p.trim()).filter((p) => p.length > 0);
|
|
1593
|
+
}
|
|
1594
|
+
if (trimmed.includes("\f")) {
|
|
1595
|
+
return trimmed.split("\f").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
1596
|
+
}
|
|
1597
|
+
return [trimmed];
|
|
1598
|
+
}
|
|
1599
|
+
function indexCitationsByPage(docId, citations) {
|
|
1600
|
+
const byPage = /* @__PURE__ */ new Map();
|
|
1601
|
+
for (const c of citations) {
|
|
1602
|
+
for (const chunk of c.chunks) {
|
|
1603
|
+
if (chunk.metadata?.doc_ext_id !== docId) continue;
|
|
1604
|
+
const page = chunk.metadata?.page_number ?? 1;
|
|
1605
|
+
const list = byPage.get(page) ?? [];
|
|
1606
|
+
if (!list.includes(c.citationNum)) list.push(c.citationNum);
|
|
1607
|
+
byPage.set(page, list);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
return byPage;
|
|
1611
|
+
}
|
|
1612
|
+
function renderDocView(opts) {
|
|
1613
|
+
const { docId, title, fileName, pages, citationsByPage } = opts;
|
|
1614
|
+
const totalCitations = Array.from(citationsByPage.values()).reduce((n, ids) => n + ids.length, 0);
|
|
1615
|
+
const lines = [];
|
|
1616
|
+
lines.push(colors.textBold(title));
|
|
1617
|
+
if (fileName && fileName !== title) lines.push(colors.muted(fileName));
|
|
1618
|
+
lines.push(colors.muted(`${docId} \u2014 ${pages.length} page${pages.length === 1 ? "" : "s"}`));
|
|
1619
|
+
if (totalCitations > 0) {
|
|
1620
|
+
lines.push(
|
|
1621
|
+
colors.muted(
|
|
1622
|
+
`${totalCitations} citation${totalCitations === 1 ? "" : "s"} from the current conversation`
|
|
1623
|
+
)
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
lines.push("");
|
|
1627
|
+
for (let i = 0; i < pages.length; i++) {
|
|
1628
|
+
const pageNum = i + 1;
|
|
1629
|
+
const cites = citationsByPage.get(pageNum);
|
|
1630
|
+
const citeBadge = cites && cites.length > 0 ? colors.accent(` [${cites.map((n) => `[${n}]`).join(" ")}]`) : "";
|
|
1631
|
+
lines.push(colors.accentBold(`\u2500\u2500 page ${pageNum} \u2500\u2500`) + citeBadge);
|
|
1632
|
+
const body = pages[i];
|
|
1633
|
+
if (body.length > MAX_CHARS_PER_PAGE) {
|
|
1634
|
+
lines.push(body.slice(0, MAX_CHARS_PER_PAGE));
|
|
1635
|
+
lines.push(
|
|
1636
|
+
colors.muted(
|
|
1637
|
+
`\u2026 (${body.length - MAX_CHARS_PER_PAGE} more chars \u2014 use /parsed ${docId} for the raw stream)`
|
|
1638
|
+
)
|
|
1639
|
+
);
|
|
1640
|
+
} else {
|
|
1641
|
+
lines.push(body);
|
|
1642
|
+
}
|
|
1643
|
+
lines.push("");
|
|
1644
|
+
}
|
|
1645
|
+
return lines;
|
|
1646
|
+
}
|
|
1647
|
+
var viewCommands = [
|
|
1648
|
+
{
|
|
1649
|
+
name: "view",
|
|
1650
|
+
description: "Read a document in the chat log, with citation hints per page",
|
|
1651
|
+
argHint: "doc-id",
|
|
1652
|
+
minArgs: 1,
|
|
1653
|
+
requires: "workspace",
|
|
1654
|
+
run: async (ctx) => {
|
|
1655
|
+
const { args, arbi, authHeaders, tui } = ctx;
|
|
1656
|
+
const docId = args[0].trim();
|
|
1657
|
+
const [docs, parsed] = await Promise.all([
|
|
1658
|
+
sdk.documents.getDocuments(arbi, [docId]),
|
|
1659
|
+
sdk.documents.getParsedContent(authHeaders, docId, "content")
|
|
1660
|
+
]);
|
|
1661
|
+
const doc = docs[0];
|
|
1662
|
+
if (!doc) return `Document ${docId} not found.`;
|
|
1663
|
+
const meta = doc.doc_metadata;
|
|
1664
|
+
const title = meta?.title ?? doc.file_name ?? docId;
|
|
1665
|
+
const content = parsed.content ?? "";
|
|
1666
|
+
if (!content) return `No parsed content available for ${docId}.`;
|
|
1667
|
+
const pages = splitIntoPages(content);
|
|
1668
|
+
const citationsByPage = tui.lastMetadata ? indexCitationsByPage(docId, sdk.resolveCitations(tui.lastMetadata)) : /* @__PURE__ */ new Map();
|
|
1669
|
+
return renderDocView({
|
|
1670
|
+
docId,
|
|
1671
|
+
title,
|
|
1672
|
+
fileName: doc.file_name ?? null,
|
|
1673
|
+
pages,
|
|
1674
|
+
citationsByPage
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
];
|
|
1679
|
+
|
|
1680
|
+
// src/commands/sources.ts
|
|
1681
|
+
function renderSourcesList(entries) {
|
|
1682
|
+
if (entries.length === 0) {
|
|
1683
|
+
return "No sources cited yet in this conversation. Ask a question first.";
|
|
1684
|
+
}
|
|
1685
|
+
const lines = [`Sources in this conversation (${entries.length}):`, ""];
|
|
1686
|
+
for (const e of entries) {
|
|
1687
|
+
const cites = e.citationNums.map((n) => `[${n}]`).join(" ");
|
|
1688
|
+
const pages = e.pages.length > 0 ? ` p.${e.pages.join(", p.")}` : "";
|
|
1689
|
+
lines.push(` ${colors.accent(cites)} ${colors.textBold(e.title)}${pages}`);
|
|
1690
|
+
lines.push(` ${colors.muted(e.docId)}`);
|
|
1691
|
+
}
|
|
1692
|
+
lines.push("");
|
|
1693
|
+
lines.push(
|
|
1694
|
+
colors.muted("Use /view <doc-id> to read a source, or /cite N for a specific passage.")
|
|
1695
|
+
);
|
|
1696
|
+
return lines;
|
|
1697
|
+
}
|
|
1698
|
+
var sourcesCommands = [
|
|
1699
|
+
{
|
|
1700
|
+
name: "sources",
|
|
1701
|
+
description: "List documents cited in this conversation",
|
|
1702
|
+
requires: "none",
|
|
1703
|
+
run: (ctx) => {
|
|
1704
|
+
const { tui } = ctx;
|
|
1705
|
+
return renderSourcesList(tui.sourcesIndex.list());
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
];
|
|
1709
|
+
var DEFAULT_LIMIT2 = 10;
|
|
1710
|
+
function formatHistoryLine(row) {
|
|
1711
|
+
const title = row.title?.trim() || colors.muted("(untitled)");
|
|
1712
|
+
const count = typeof row.message_count === "number" ? ` \xB7 ${row.message_count} msg` : "";
|
|
1713
|
+
const when = row.updated_at ? ` \xB7 ${formatRelativeTime(row.updated_at)}` : "";
|
|
1714
|
+
return ` ${colors.accent(row.external_id)} ${title}${colors.muted(count + when)}`;
|
|
1715
|
+
}
|
|
1716
|
+
function formatRelativeTime(iso, now = /* @__PURE__ */ new Date()) {
|
|
1717
|
+
const t = new Date(iso);
|
|
1718
|
+
if (Number.isNaN(t.getTime())) return iso;
|
|
1719
|
+
const seconds = Math.floor((now.getTime() - t.getTime()) / 1e3);
|
|
1720
|
+
if (seconds < 60) return "just now";
|
|
1721
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
|
1722
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
|
|
1723
|
+
if (seconds < 86400 * 2) return "yesterday";
|
|
1724
|
+
if (seconds < 86400 * 7) return `${Math.floor(seconds / 86400)}d ago`;
|
|
1725
|
+
return t.toLocaleDateString(void 0, { month: "short", day: "numeric" });
|
|
1726
|
+
}
|
|
1727
|
+
var historyCommands = [
|
|
1728
|
+
{
|
|
1729
|
+
name: "history",
|
|
1730
|
+
description: "Browse past conversations",
|
|
1731
|
+
argHint: "count|all",
|
|
1732
|
+
requires: "workspace",
|
|
1733
|
+
run: async (ctx) => {
|
|
1734
|
+
const { arbi, args } = ctx;
|
|
1735
|
+
const arg = args[0]?.trim().toLowerCase();
|
|
1736
|
+
const convs = await sdk.conversations.listConversations(arbi);
|
|
1737
|
+
if (convs.length === 0) {
|
|
1738
|
+
return "No conversations yet in this workspace. Ask a question to start one.";
|
|
1739
|
+
}
|
|
1740
|
+
const slice = arg === "all" ? convs : convs.slice(0, parseLimit(arg) ?? DEFAULT_LIMIT2);
|
|
1741
|
+
const lines = [
|
|
1742
|
+
`Conversations (${slice.length}${slice.length < convs.length ? ` of ${convs.length}` : ""}):`,
|
|
1743
|
+
""
|
|
1744
|
+
];
|
|
1745
|
+
for (const row of slice) lines.push(formatHistoryLine(row));
|
|
1746
|
+
lines.push("");
|
|
1747
|
+
lines.push(colors.muted("Use /resume <conv-id> to switch to one of these."));
|
|
1748
|
+
return lines;
|
|
1749
|
+
}
|
|
1750
|
+
},
|
|
1751
|
+
{
|
|
1752
|
+
name: "resume",
|
|
1753
|
+
description: "Switch to a past conversation by ID",
|
|
1754
|
+
argHint: "conv-id",
|
|
1755
|
+
minArgs: 1,
|
|
1756
|
+
requires: "workspace",
|
|
1757
|
+
run: async (ctx) => {
|
|
1758
|
+
const { args, arbi, tui } = ctx;
|
|
1759
|
+
const convId = args[0].trim();
|
|
1760
|
+
const data = await sdk.conversations.getConversationThreads(arbi, convId);
|
|
1761
|
+
const threadList = data.threads ?? [];
|
|
1762
|
+
const primary = threadList[0];
|
|
1763
|
+
if (!primary?.leaf_message_ext_id) {
|
|
1764
|
+
return `Conversation ${convId} not found or has no messages.`;
|
|
1765
|
+
}
|
|
1766
|
+
tui.chatLog.clearMessages();
|
|
1767
|
+
for (const msg of primary.history ?? []) {
|
|
1768
|
+
if (msg.role === "user") tui.chatLog.addUser(msg.content);
|
|
1769
|
+
else if (msg.role === "assistant") tui.chatLog.addAssistant(msg.content);
|
|
1770
|
+
}
|
|
1771
|
+
tui.chatLog.addSystem(`--- resumed conversation ${convId} ---`, "info");
|
|
1772
|
+
tui.state.conversationMessageId = primary.leaf_message_ext_id;
|
|
1773
|
+
tui.store.updateChatSession({
|
|
1774
|
+
lastMessageExtId: primary.leaf_message_ext_id,
|
|
1775
|
+
conversationExtId: convId
|
|
1776
|
+
});
|
|
1777
|
+
tui.sourcesIndex.clear();
|
|
1778
|
+
tui.lastMetadata = null;
|
|
1779
|
+
tui.requestRender();
|
|
1780
|
+
return `Resumed conversation ${convId}.`;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
];
|
|
1784
|
+
function parseLimit(arg) {
|
|
1785
|
+
if (!arg) return void 0;
|
|
1786
|
+
const n = Number.parseInt(arg, 10);
|
|
1787
|
+
return Number.isFinite(n) && n > 0 ? n : void 0;
|
|
1788
|
+
}
|
|
1789
|
+
init_tui_helpers();
|
|
1790
|
+
var skillCommands = [
|
|
1791
|
+
{
|
|
1792
|
+
name: "skills",
|
|
1793
|
+
description: "List user-invocable skills in the active workspace",
|
|
1794
|
+
requires: "workspace",
|
|
1795
|
+
run: async (ctx) => {
|
|
1796
|
+
const { arbi, tui } = ctx;
|
|
1797
|
+
const list = await sdk.assistant.listSkills(arbi);
|
|
1798
|
+
tui.skillsCache = list;
|
|
1799
|
+
if (list.length === 0) {
|
|
1800
|
+
return [
|
|
1801
|
+
"No skills in this workspace yet.",
|
|
1802
|
+
colors.muted(
|
|
1803
|
+
"Skills are documents with wp_type=skill. Upload a SKILL.md with frontmatter (name, description, user-invocable: true) to add one."
|
|
1804
|
+
)
|
|
1805
|
+
];
|
|
1806
|
+
}
|
|
1807
|
+
const lines = [`Skills (${list.length}):`, ""];
|
|
1808
|
+
for (const s of list) {
|
|
1809
|
+
const argHint = s.arg_hint ? colors.muted(` ${s.arg_hint}`) : "";
|
|
1810
|
+
const typeBadge = s.skill_type && s.skill_type !== "markdown" ? colors.muted(` [${s.skill_type}]`) : "";
|
|
1811
|
+
lines.push(` ${colors.accent("/" + s.slug)}${argHint}${typeBadge}`);
|
|
1812
|
+
if (s.description) {
|
|
1813
|
+
lines.push(` ${colors.muted(s.description)}`);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
lines.push("");
|
|
1817
|
+
lines.push(
|
|
1818
|
+
colors.muted("Use /skill view <slug> to read a skill, /skill source <slug> for its doc-id.")
|
|
1819
|
+
);
|
|
1820
|
+
return lines;
|
|
1821
|
+
}
|
|
1822
|
+
},
|
|
1823
|
+
{
|
|
1824
|
+
name: "skill",
|
|
1825
|
+
description: "Inspect or edit a skill (view | source | edit) <slug>",
|
|
1826
|
+
argHint: "subcommand slug",
|
|
1827
|
+
minArgs: 2,
|
|
1828
|
+
requires: "workspace",
|
|
1829
|
+
run: async (ctx) => {
|
|
1830
|
+
const { args, arbi, tui } = ctx;
|
|
1831
|
+
const [sub, slug, ...rest] = args;
|
|
1832
|
+
if (sub !== "view" && sub !== "source" && sub !== "edit") {
|
|
1833
|
+
return `Unknown /skill subcommand "${sub}". Use /skill view <slug>, /skill source <slug>, or /skill edit <slug>.`;
|
|
1834
|
+
}
|
|
1835
|
+
const list = tui.skillsCache?.length ? tui.skillsCache : await sdk.assistant.listSkills(arbi, { includeHidden: true });
|
|
1836
|
+
const skill = list.find((s) => s.slug === slug || s.name === slug);
|
|
1837
|
+
if (!skill) {
|
|
1838
|
+
return `No skill matching "${slug}" in this workspace. Use /skills to list available skills.`;
|
|
1839
|
+
}
|
|
1840
|
+
if (sub === "source") {
|
|
1841
|
+
return [
|
|
1842
|
+
colors.textBold(`${skill.name} (${skill.slug})`),
|
|
1843
|
+
colors.muted(`doc-id: ${skill.doc_ext_id}`),
|
|
1844
|
+
colors.muted(`workspace: ${skill.workspace_ext_id}`),
|
|
1845
|
+
colors.muted(
|
|
1846
|
+
"Use `arbi upload --folder skills/<slug>` to replace the SKILL.md, or edit it via the web app."
|
|
1847
|
+
)
|
|
1848
|
+
];
|
|
1849
|
+
}
|
|
1850
|
+
const { authHeaders } = ctx;
|
|
1851
|
+
if (sub === "edit") {
|
|
1852
|
+
return await editSkill(tui, authHeaders, skill);
|
|
1853
|
+
}
|
|
1854
|
+
const dlRes = await sdk.documents.downloadDocument(authHeaders, skill.doc_ext_id);
|
|
1855
|
+
const body = dlRes.ok ? await dlRes.text() : "";
|
|
1856
|
+
const lines = [colors.textBold(`${skill.name} (${skill.slug})`)];
|
|
1857
|
+
if (skill.description) lines.push(colors.muted(skill.description));
|
|
1858
|
+
if (skill.arg_hint) lines.push(colors.muted(`args: ${skill.arg_hint}`));
|
|
1859
|
+
lines.push(colors.muted(`type: ${skill.skill_type} doc: ${skill.doc_ext_id}`));
|
|
1860
|
+
lines.push("");
|
|
1861
|
+
lines.push(body);
|
|
1862
|
+
return lines;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
];
|
|
1866
|
+
async function editSkill(tui, authHeaders, skill) {
|
|
1867
|
+
const dlRes = await sdk.documents.downloadDocument(authHeaders, skill.doc_ext_id);
|
|
1868
|
+
if (!dlRes.ok) {
|
|
1869
|
+
return `Failed to fetch skill body (HTTP ${dlRes.status}). Edit aborted.`;
|
|
1870
|
+
}
|
|
1871
|
+
const original = await dlRes.text();
|
|
1872
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `arbi-skill-${skill.slug}-`));
|
|
1873
|
+
const path$1 = path.join(dir, "SKILL.md");
|
|
1874
|
+
fs.writeFileSync(path$1, original, "utf8");
|
|
1875
|
+
const mtimeBefore = fs.statSync(path$1).mtimeMs;
|
|
1876
|
+
const editor = process.env.VISUAL || process.env.EDITOR || "vi";
|
|
1877
|
+
const result = await runInteractiveFlow(
|
|
1878
|
+
tui,
|
|
1879
|
+
colors.muted(`Opening ${skill.slug}/SKILL.md in ${editor}\u2026`),
|
|
1880
|
+
async () => {
|
|
1881
|
+
const child = child_process.spawnSync(editor, [path$1], { stdio: "inherit" });
|
|
1882
|
+
if (child.error) throw child.error;
|
|
1883
|
+
if (child.status !== 0 && child.status !== null) {
|
|
1884
|
+
throw new Error(`${editor} exited with status ${child.status}`);
|
|
1885
|
+
}
|
|
1886
|
+
return fs.readFileSync(path$1, "utf8");
|
|
1887
|
+
},
|
|
1888
|
+
`Skill edit (${skill.slug}) failed`
|
|
1889
|
+
);
|
|
1890
|
+
const mtimeAfter = (() => {
|
|
1891
|
+
try {
|
|
1892
|
+
return fs.statSync(path$1).mtimeMs;
|
|
1893
|
+
} catch {
|
|
1894
|
+
return mtimeBefore;
|
|
1895
|
+
}
|
|
1896
|
+
})();
|
|
1897
|
+
const updated = result ?? original;
|
|
1898
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1899
|
+
if (result === null) {
|
|
1900
|
+
return [];
|
|
1901
|
+
}
|
|
1902
|
+
if (mtimeAfter === mtimeBefore || updated === original) {
|
|
1903
|
+
return colors.muted(`No changes to ${skill.slug}/SKILL.md \u2014 skill unchanged.`);
|
|
1904
|
+
}
|
|
1905
|
+
const blob = new Blob([updated], { type: "text/markdown" });
|
|
1906
|
+
await sdk.documents.uploadFile(authHeaders, blob, "SKILL.md", {
|
|
1907
|
+
folder: `skills/${skill.slug}`,
|
|
1908
|
+
wpType: "skill"
|
|
1909
|
+
});
|
|
1910
|
+
await tui.refreshSkillsCache();
|
|
1911
|
+
return [
|
|
1912
|
+
colors.textBold(`Updated ${skill.name} (${skill.slug})`),
|
|
1913
|
+
colors.muted("Skill cache refreshed \u2014 new description and args take effect immediately.")
|
|
1914
|
+
];
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1339
1917
|
// src/commands/index.ts
|
|
1340
1918
|
function registerAllCommands() {
|
|
1341
1919
|
registerCommands(generalCommands);
|
|
@@ -1346,6 +1924,21 @@ function registerAllCommands() {
|
|
|
1346
1924
|
registerCommands(tagCommands);
|
|
1347
1925
|
registerCommands(miscCommands);
|
|
1348
1926
|
registerCommands(citationCommands);
|
|
1927
|
+
registerCommands(eventCommands);
|
|
1928
|
+
registerCommands(viewCommands);
|
|
1929
|
+
registerCommands(sourcesCommands);
|
|
1930
|
+
registerCommands(historyCommands);
|
|
1931
|
+
registerCommands(skillCommands);
|
|
1932
|
+
}
|
|
1933
|
+
function extractStepDetails(data) {
|
|
1934
|
+
const detail = data.detail;
|
|
1935
|
+
if (!detail || detail.length === 0) return void 0;
|
|
1936
|
+
const d = detail[0];
|
|
1937
|
+
if (!d) return void 0;
|
|
1938
|
+
const result = {};
|
|
1939
|
+
if (d.tool) result.tool = d.tool;
|
|
1940
|
+
if (d.message) result.args = d.message;
|
|
1941
|
+
return result.tool || result.args || result.result ? result : void 0;
|
|
1349
1942
|
}
|
|
1350
1943
|
async function streamResponse(tui, response) {
|
|
1351
1944
|
tui.chatLog.startAssistant();
|
|
@@ -1372,7 +1965,7 @@ async function streamResponse(tui, response) {
|
|
|
1372
1965
|
onAgentStep: (data) => {
|
|
1373
1966
|
const label = sdk.formatAgentStepLabel(data);
|
|
1374
1967
|
if (label) {
|
|
1375
|
-
tui.chatLog.addAgentStep(label);
|
|
1968
|
+
tui.chatLog.addAgentStep(label, extractStepDetails(data));
|
|
1376
1969
|
tui.requestRender();
|
|
1377
1970
|
}
|
|
1378
1971
|
},
|
|
@@ -1388,6 +1981,7 @@ async function streamResponse(tui, response) {
|
|
|
1388
1981
|
const result = await sdk.streamSSE(response, callbacks);
|
|
1389
1982
|
tui.chatLog.finalizeAssistant();
|
|
1390
1983
|
tui.lastMetadata = result.metadata ?? null;
|
|
1984
|
+
tui.sourcesIndex.recordCitations(result.metadata);
|
|
1391
1985
|
const summary = sdk.formatStreamSummary(result, elapsedTime);
|
|
1392
1986
|
if (summary) {
|
|
1393
1987
|
const refs = sdk.countCitations(result.metadata ?? null);
|
|
@@ -1596,43 +2190,50 @@ var DURATION_BY_LEVEL = {
|
|
|
1596
2190
|
function isNotification(msg) {
|
|
1597
2191
|
return "sender" in msg && "recipient" in msg;
|
|
1598
2192
|
}
|
|
1599
|
-
|
|
2193
|
+
function shouldToast(level, msgType) {
|
|
2194
|
+
if (level === "error" || level === "warning" || level === "success") return true;
|
|
2195
|
+
if (msgType === "response_complete" || msgType === "batch_complete") return true;
|
|
2196
|
+
return false;
|
|
2197
|
+
}
|
|
2198
|
+
async function handleMessage(msg, toasts, eventLog, tui) {
|
|
1600
2199
|
if (isNotification(msg) && msg.type === "user_message" && tui) {
|
|
1601
2200
|
const handled = await handleIncomingDm(tui, msg);
|
|
1602
2201
|
if (handled) return;
|
|
1603
2202
|
}
|
|
1604
2203
|
const { text, level } = sdk.formatWsMessage(msg);
|
|
1605
|
-
const
|
|
1606
|
-
|
|
2204
|
+
const msgType = msg.type;
|
|
2205
|
+
eventLog.add({ text, level, msgType });
|
|
2206
|
+
if (shouldToast(level, msgType)) {
|
|
2207
|
+
const duration = DURATION_BY_LEVEL[level] ?? DURATION_DEFAULT;
|
|
2208
|
+
toasts.show(text, level, duration);
|
|
2209
|
+
}
|
|
1607
2210
|
}
|
|
1608
2211
|
async function connectTuiWebSocket(options) {
|
|
1609
|
-
const { baseUrl, accessToken, toasts, tui } = options;
|
|
2212
|
+
const { baseUrl, accessToken, toasts, eventLog, tui } = options;
|
|
2213
|
+
const recordTransport = (text, level) => {
|
|
2214
|
+
eventLog.add({ text, level });
|
|
2215
|
+
const duration = DURATION_BY_LEVEL[level] ?? DURATION_DEFAULT;
|
|
2216
|
+
toasts.show(text, level, duration);
|
|
2217
|
+
};
|
|
1610
2218
|
try {
|
|
1611
2219
|
const connection = await sdk.connectWithReconnect({
|
|
1612
2220
|
baseUrl,
|
|
1613
2221
|
accessToken,
|
|
1614
|
-
onMessage: (msg) => handleMessage(msg, toasts, tui ?? null),
|
|
1615
|
-
onClose: () =>
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
toasts.show(`Reconnecting... (${attempt}/${maxRetries})`, "warning", DURATION_DEFAULT);
|
|
1620
|
-
},
|
|
1621
|
-
onReconnected: () => {
|
|
1622
|
-
toasts.show("WebSocket reconnected", "success", DURATION_DEFAULT);
|
|
1623
|
-
},
|
|
1624
|
-
onReconnectFailed: () => {
|
|
1625
|
-
toasts.show("WebSocket reconnection failed", "error", DURATION_LONG);
|
|
1626
|
-
}
|
|
2222
|
+
onMessage: (msg) => handleMessage(msg, toasts, eventLog, tui ?? null),
|
|
2223
|
+
onClose: () => recordTransport("WebSocket disconnected", "warning"),
|
|
2224
|
+
onReconnecting: (attempt, maxRetries) => recordTransport(`Reconnecting... (${attempt}/${maxRetries})`, "warning"),
|
|
2225
|
+
onReconnected: () => recordTransport("WebSocket reconnected", "success"),
|
|
2226
|
+
onReconnectFailed: () => recordTransport("WebSocket reconnection failed", "error")
|
|
1627
2227
|
});
|
|
1628
2228
|
return connection;
|
|
1629
2229
|
} catch {
|
|
1630
|
-
|
|
2230
|
+
recordTransport("WebSocket connection failed", "warning");
|
|
1631
2231
|
return null;
|
|
1632
2232
|
}
|
|
1633
2233
|
}
|
|
1634
2234
|
|
|
1635
2235
|
// src/tui.ts
|
|
2236
|
+
init_tui_helpers();
|
|
1636
2237
|
var ArbiTui = class {
|
|
1637
2238
|
tui;
|
|
1638
2239
|
header;
|
|
@@ -1658,6 +2259,23 @@ var ArbiTui = class {
|
|
|
1658
2259
|
dmChannel = null;
|
|
1659
2260
|
/** Last response metadata — used by /cite for citation browsing. */
|
|
1660
2261
|
lastMetadata = null;
|
|
2262
|
+
/** Persistent log of WebSocket events for this session. Toasts are
|
|
2263
|
+
* ephemeral; this is the "what happened?" scrollback that
|
|
2264
|
+
* ``/events`` reads from. Public so ws-handler can append and the
|
|
2265
|
+
* events command can read. */
|
|
2266
|
+
eventLog = new WsEventLog();
|
|
2267
|
+
/** Running tally of documents cited in the current conversation.
|
|
2268
|
+
* Cleared on ``/new``; appended to after every assistant response
|
|
2269
|
+
* that carries citation metadata. Drives ``/sources`` and (later)
|
|
2270
|
+
* the persistent right-side sources panel. */
|
|
2271
|
+
sourcesIndex = new SourcesIndex();
|
|
2272
|
+
/** Cached list of user-invocable skills in the active workspace.
|
|
2273
|
+
* Refreshed on workspace switch (``setWorkspaceContext``) and
|
|
2274
|
+
* invalidated to ``[]`` when leaving a workspace. Drives the
|
|
2275
|
+
* autocomplete provider and the ``/skills`` / ``/skill view``
|
|
2276
|
+
* commands. ``null`` means "haven't fetched yet"; ``[]`` means
|
|
2277
|
+
* "fetched, no skills exist". */
|
|
2278
|
+
skillsCache = [];
|
|
1661
2279
|
/** Active citation overlay handle (null when not showing). */
|
|
1662
2280
|
citationOverlay = null;
|
|
1663
2281
|
/** Input listener ID for overlay dismiss (null when no overlay). */
|
|
@@ -1728,15 +2346,60 @@ var ArbiTui = class {
|
|
|
1728
2346
|
/** Create and wire a new editor instance. */
|
|
1729
2347
|
createEditor() {
|
|
1730
2348
|
const editor = new ArbiEditor(this.tui);
|
|
1731
|
-
editor.setAutocompleteProvider(
|
|
2349
|
+
editor.setAutocompleteProvider(
|
|
2350
|
+
new piTui.CombinedAutocompleteProvider(toSlashCommands(), process.cwd())
|
|
2351
|
+
);
|
|
1732
2352
|
editor.onSubmit = (text) => this.handleSubmit(text);
|
|
1733
2353
|
editor.onEscape = () => this.handleAbort();
|
|
1734
2354
|
editor.onCtrlC = () => this.shutdown();
|
|
1735
2355
|
editor.onCtrlD = () => this.shutdown();
|
|
1736
2356
|
editor.onCtrlW = () => this.handleSubmit("/workspaces");
|
|
1737
2357
|
editor.onCtrlN = () => this.handleSubmit("/new");
|
|
2358
|
+
editor.onCitationKey = (n) => this.handleSubmit(`/cite ${n}`);
|
|
2359
|
+
editor.onBufferChange = (text) => this.handleBufferChange(text);
|
|
1738
2360
|
return editor;
|
|
1739
2361
|
}
|
|
2362
|
+
/** The slug we last surfaced as a pre-flight hint. ``null`` once
|
|
2363
|
+
* the user has navigated away from the skill prefix (e.g. typed a
|
|
2364
|
+
* non-slash char first, or deleted past the slash). Tracking this
|
|
2365
|
+
* here — not in the editor — keeps the editor pure: it just emits
|
|
2366
|
+
* text, the TUI decides what's interesting. */
|
|
2367
|
+
lastHintedSlug = null;
|
|
2368
|
+
/**
|
|
2369
|
+
* Pre-flight skill hint dispatcher.
|
|
2370
|
+
*
|
|
2371
|
+
* Watches the editor buffer; when it starts with ``/<token>`` where
|
|
2372
|
+
* ``<token>`` is a slug in ``skillsCache`` *and* not also a static
|
|
2373
|
+
* command (those have their own ``--help`` style help), we drop a
|
|
2374
|
+
* one-line description into the chat log so the user sees what
|
|
2375
|
+
* they're about to invoke before pressing Enter. Equivalent of
|
|
2376
|
+
* claude-code's slash-command palette preview, minus the modal.
|
|
2377
|
+
*
|
|
2378
|
+
* Dedup rule: only emit when the matched slug *changes*. Typing
|
|
2379
|
+
* "/foo" → "/foob" → "/foo" emits once for ``foo``, never for the
|
|
2380
|
+
* intermediate ``foob`` (no match), and not again when the user
|
|
2381
|
+
* lands back on ``foo``. The state resets whenever the buffer no
|
|
2382
|
+
* longer matches anything, so a fresh ``/foo`` after a clear emits
|
|
2383
|
+
* the hint again.
|
|
2384
|
+
*/
|
|
2385
|
+
handleBufferChange(text) {
|
|
2386
|
+
const parsed = sdk.parseSlashCommand(text);
|
|
2387
|
+
if (!parsed) {
|
|
2388
|
+
this.lastHintedSlug = null;
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
const slug = parsed.slug;
|
|
2392
|
+
if (this.skillsCache.length === 0) return;
|
|
2393
|
+
const skill = this.skillsCache.find((s) => s.slug === slug);
|
|
2394
|
+
if (!skill) {
|
|
2395
|
+
this.lastHintedSlug = null;
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
if (this.lastHintedSlug === slug) return;
|
|
2399
|
+
this.lastHintedSlug = slug;
|
|
2400
|
+
const argHint = skill.arg_hint ? ` ${skill.arg_hint}` : "";
|
|
2401
|
+
showMessage(this, `Skill: /${skill.slug}${argHint} \u2014 ${skill.description}`, "info");
|
|
2402
|
+
}
|
|
1740
2403
|
/** Request a render update. */
|
|
1741
2404
|
requestRender() {
|
|
1742
2405
|
this.tui.requestRender();
|
|
@@ -1757,6 +2420,34 @@ var ArbiTui = class {
|
|
|
1757
2420
|
this.state.workspaceId = ctx.workspaceId;
|
|
1758
2421
|
this.updateHeader();
|
|
1759
2422
|
this.connectWebSocket();
|
|
2423
|
+
void this.refreshSkillsCache();
|
|
2424
|
+
}
|
|
2425
|
+
/** Refresh the cached skills list for the current workspace. Called
|
|
2426
|
+
* on workspace switch and from ``/skills`` itself so a freshly
|
|
2427
|
+
* uploaded skill becomes invocable without a TUI restart. */
|
|
2428
|
+
async refreshSkillsCache() {
|
|
2429
|
+
if (!this.workspaceContext) {
|
|
2430
|
+
this.skillsCache = [];
|
|
2431
|
+
this.rebuildAutocomplete();
|
|
2432
|
+
return;
|
|
2433
|
+
}
|
|
2434
|
+
try {
|
|
2435
|
+
this.skillsCache = await sdk.assistant.listSkills(this.workspaceContext.arbi);
|
|
2436
|
+
} catch {
|
|
2437
|
+
this.skillsCache = [];
|
|
2438
|
+
}
|
|
2439
|
+
this.rebuildAutocomplete();
|
|
2440
|
+
}
|
|
2441
|
+
/** Re-feed the editor's autocomplete provider with the merged
|
|
2442
|
+
* static-commands + skills list. Called on workspace switch and
|
|
2443
|
+
* after every ``refreshSkillsCache`` so a newly uploaded skill
|
|
2444
|
+
* appears in tab-complete without a TUI restart. */
|
|
2445
|
+
rebuildAutocomplete() {
|
|
2446
|
+
if (!this.editor) return;
|
|
2447
|
+
const merged = toSlashCommandsWithSkills(
|
|
2448
|
+
this.skillsCache.map((s) => ({ slug: s.slug, description: s.description }))
|
|
2449
|
+
);
|
|
2450
|
+
this.editor.setAutocompleteProvider(new piTui.CombinedAutocompleteProvider(merged, process.cwd()));
|
|
1760
2451
|
}
|
|
1761
2452
|
/** Refresh workspace context (after switching workspaces). */
|
|
1762
2453
|
async refreshWorkspaceContext() {
|
|
@@ -1784,6 +2475,7 @@ var ArbiTui = class {
|
|
|
1784
2475
|
baseUrl: config.baseUrl,
|
|
1785
2476
|
accessToken,
|
|
1786
2477
|
toasts: this.toastContainer,
|
|
2478
|
+
eventLog: this.eventLog,
|
|
1787
2479
|
tui: this
|
|
1788
2480
|
}).then((conn) => {
|
|
1789
2481
|
this.wsConnection = conn;
|
|
@@ -1841,6 +2533,14 @@ var ArbiTui = class {
|
|
|
1841
2533
|
previousResponseId
|
|
1842
2534
|
});
|
|
1843
2535
|
const result = await streamResponse(this, response);
|
|
2536
|
+
if (this.lastMetadata) {
|
|
2537
|
+
const n = sdk.countCitations(this.lastMetadata);
|
|
2538
|
+
if (n > 0) {
|
|
2539
|
+
const cap = Math.min(n, 9);
|
|
2540
|
+
const label = n <= 9 ? `${n} citation${n === 1 ? "" : "s"} available` : `${n} citations \u2014 Alt+1..9 jumps to the first 9; /cite N for the rest`;
|
|
2541
|
+
this.chatLog.addSystem(`${label} \u2014 press Alt+1..${cap} to view`, "info");
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
1844
2544
|
if (result.assistantMessageExtId) {
|
|
1845
2545
|
this.state.conversationMessageId = result.assistantMessageExtId;
|
|
1846
2546
|
const sessionUpdate = {
|