@arbidocs/tui 0.3.66 → 0.3.67
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 +732 -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 = {
|
|
@@ -423,6 +594,15 @@ function toSlashCommands() {
|
|
|
423
594
|
}
|
|
424
595
|
return cmds;
|
|
425
596
|
}
|
|
597
|
+
function toSlashCommandsWithSkills(skills) {
|
|
598
|
+
const out = toSlashCommands();
|
|
599
|
+
const taken = new Set(out.map((c) => c.name));
|
|
600
|
+
for (const s of skills) {
|
|
601
|
+
if (taken.has(s.slug)) continue;
|
|
602
|
+
out.push({ name: s.slug, description: s.description });
|
|
603
|
+
}
|
|
604
|
+
return out;
|
|
605
|
+
}
|
|
426
606
|
function formatHelpText() {
|
|
427
607
|
const lines = [];
|
|
428
608
|
for (const def of registry.values()) {
|
|
@@ -440,21 +620,33 @@ function formatHelpText() {
|
|
|
440
620
|
" @arbi \u2014 Switch back to AI chat",
|
|
441
621
|
"",
|
|
442
622
|
"Keyboard shortcuts:",
|
|
443
|
-
" Ctrl+N
|
|
444
|
-
" Ctrl+W
|
|
445
|
-
" Ctrl+D
|
|
446
|
-
" Escape
|
|
623
|
+
" Ctrl+N \u2014 New conversation",
|
|
624
|
+
" Ctrl+W \u2014 Switch workspace",
|
|
625
|
+
" Ctrl+D \u2014 Exit",
|
|
626
|
+
" Escape \u2014 Abort streaming",
|
|
627
|
+
" Alt+1..9 \u2014 Jump to citation N from the last response",
|
|
628
|
+
"",
|
|
629
|
+
"Skills: any workspace SKILL.md with `user-invocable: true` becomes a",
|
|
630
|
+
" ``/<slug>`` command \u2014 type it like a regular slash command and the",
|
|
631
|
+
" agent handles it. ``/skills`` lists what is available."
|
|
447
632
|
].join("\n");
|
|
448
633
|
}
|
|
449
634
|
async function dispatchCommand(tui, input2) {
|
|
635
|
+
const parsed = sdk.parseSlashCommand(input2);
|
|
636
|
+
if (!parsed) return false;
|
|
637
|
+
const cmdName = parsed.slug;
|
|
638
|
+
const args = parsed.args.length > 0 ? parsed.args.split(/\s+/) : [];
|
|
450
639
|
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
640
|
const def = registry.get(cmdName);
|
|
457
641
|
if (!def) {
|
|
642
|
+
const skill = tui.skillsCache?.find(
|
|
643
|
+
(s) => s.slug === cmdName || s.name.toLowerCase() === cmdName
|
|
644
|
+
);
|
|
645
|
+
if (skill) {
|
|
646
|
+
const meta = skill.arg_hint ? `Skill: ${skill.name} ${skill.arg_hint} \u2014 ${skill.description}` : `Skill: ${skill.name} \u2014 ${skill.description}`;
|
|
647
|
+
showMessage(tui, meta, "info");
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
458
650
|
showMessage(tui, `Unknown command: /${cmdName}. Type /help for available commands.`, "warning");
|
|
459
651
|
return true;
|
|
460
652
|
}
|
|
@@ -548,6 +740,7 @@ var generalCommands = [
|
|
|
548
740
|
const { tui } = ctx;
|
|
549
741
|
tui.state.conversationMessageId = null;
|
|
550
742
|
tui.lastMetadata = null;
|
|
743
|
+
tui.sourcesIndex.clear();
|
|
551
744
|
tui.store.clearChatSession();
|
|
552
745
|
return "Started new conversation.";
|
|
553
746
|
}
|
|
@@ -1336,6 +1529,387 @@ var citationCommands = [
|
|
|
1336
1529
|
}
|
|
1337
1530
|
];
|
|
1338
1531
|
|
|
1532
|
+
// src/commands/events.ts
|
|
1533
|
+
var DEFAULT_LIMIT = 20;
|
|
1534
|
+
function parseEventsArg(arg) {
|
|
1535
|
+
if (!arg) return { limit: DEFAULT_LIMIT };
|
|
1536
|
+
const lower = arg.trim().toLowerCase();
|
|
1537
|
+
if (lower === "all") return {};
|
|
1538
|
+
if (lower === "errors" || lower === "error") {
|
|
1539
|
+
return { limit: DEFAULT_LIMIT, level: "error" };
|
|
1540
|
+
}
|
|
1541
|
+
if (lower === "warnings" || lower === "warning") {
|
|
1542
|
+
return { limit: DEFAULT_LIMIT, level: "warning" };
|
|
1543
|
+
}
|
|
1544
|
+
const n = Number.parseInt(lower, 10);
|
|
1545
|
+
if (Number.isFinite(n) && n > 0) return { limit: n };
|
|
1546
|
+
return { limit: DEFAULT_LIMIT };
|
|
1547
|
+
}
|
|
1548
|
+
var eventCommands = [
|
|
1549
|
+
{
|
|
1550
|
+
name: "events",
|
|
1551
|
+
description: "Show recent WebSocket events (replaces toasts that scroll off)",
|
|
1552
|
+
argHint: "count|all|errors",
|
|
1553
|
+
requires: "none",
|
|
1554
|
+
run: (ctx) => {
|
|
1555
|
+
const { tui, args } = ctx;
|
|
1556
|
+
const query = parseEventsArg(args[0]);
|
|
1557
|
+
const entries = tui.eventLog.getAll(query);
|
|
1558
|
+
if (entries.length === 0) {
|
|
1559
|
+
if (query.level) {
|
|
1560
|
+
return `No ${query.level} events recorded yet.`;
|
|
1561
|
+
}
|
|
1562
|
+
return "No WebSocket events recorded yet.";
|
|
1563
|
+
}
|
|
1564
|
+
const lines = entries.map((e) => {
|
|
1565
|
+
const line = formatEventLine(e);
|
|
1566
|
+
switch (e.level) {
|
|
1567
|
+
case "error":
|
|
1568
|
+
return colors.error(line);
|
|
1569
|
+
case "warning":
|
|
1570
|
+
return colors.warning(line);
|
|
1571
|
+
case "success":
|
|
1572
|
+
return colors.success(line);
|
|
1573
|
+
default:
|
|
1574
|
+
return colors.muted(line);
|
|
1575
|
+
}
|
|
1576
|
+
});
|
|
1577
|
+
const header = query.level != null ? `Events (${entries.length}, level=${query.level}):` : `Events (${entries.length} of ${tui.eventLog.size} retained):`;
|
|
1578
|
+
return [header, "", ...lines];
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
];
|
|
1582
|
+
var MAX_CHARS_PER_PAGE = 2e3;
|
|
1583
|
+
function splitIntoPages(content) {
|
|
1584
|
+
if (!content) return [];
|
|
1585
|
+
const trimmed = content.trim();
|
|
1586
|
+
if (!trimmed) return [];
|
|
1587
|
+
if (trimmed.includes("\n---\n")) {
|
|
1588
|
+
return trimmed.split(/\n---\n/).map((p) => p.trim()).filter((p) => p.length > 0);
|
|
1589
|
+
}
|
|
1590
|
+
if (trimmed.includes("\f")) {
|
|
1591
|
+
return trimmed.split("\f").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
1592
|
+
}
|
|
1593
|
+
return [trimmed];
|
|
1594
|
+
}
|
|
1595
|
+
function indexCitationsByPage(docId, citations) {
|
|
1596
|
+
const byPage = /* @__PURE__ */ new Map();
|
|
1597
|
+
for (const c of citations) {
|
|
1598
|
+
for (const chunk of c.chunks) {
|
|
1599
|
+
if (chunk.metadata?.doc_ext_id !== docId) continue;
|
|
1600
|
+
const page = chunk.metadata?.page_number ?? 1;
|
|
1601
|
+
const list = byPage.get(page) ?? [];
|
|
1602
|
+
if (!list.includes(c.citationNum)) list.push(c.citationNum);
|
|
1603
|
+
byPage.set(page, list);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
return byPage;
|
|
1607
|
+
}
|
|
1608
|
+
function renderDocView(opts) {
|
|
1609
|
+
const { docId, title, fileName, pages, citationsByPage } = opts;
|
|
1610
|
+
const totalCitations = Array.from(citationsByPage.values()).reduce((n, ids) => n + ids.length, 0);
|
|
1611
|
+
const lines = [];
|
|
1612
|
+
lines.push(colors.textBold(title));
|
|
1613
|
+
if (fileName && fileName !== title) lines.push(colors.muted(fileName));
|
|
1614
|
+
lines.push(colors.muted(`${docId} \u2014 ${pages.length} page${pages.length === 1 ? "" : "s"}`));
|
|
1615
|
+
if (totalCitations > 0) {
|
|
1616
|
+
lines.push(
|
|
1617
|
+
colors.muted(
|
|
1618
|
+
`${totalCitations} citation${totalCitations === 1 ? "" : "s"} from the current conversation`
|
|
1619
|
+
)
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
lines.push("");
|
|
1623
|
+
for (let i = 0; i < pages.length; i++) {
|
|
1624
|
+
const pageNum = i + 1;
|
|
1625
|
+
const cites = citationsByPage.get(pageNum);
|
|
1626
|
+
const citeBadge = cites && cites.length > 0 ? colors.accent(` [${cites.map((n) => `[${n}]`).join(" ")}]`) : "";
|
|
1627
|
+
lines.push(colors.accentBold(`\u2500\u2500 page ${pageNum} \u2500\u2500`) + citeBadge);
|
|
1628
|
+
const body = pages[i];
|
|
1629
|
+
if (body.length > MAX_CHARS_PER_PAGE) {
|
|
1630
|
+
lines.push(body.slice(0, MAX_CHARS_PER_PAGE));
|
|
1631
|
+
lines.push(
|
|
1632
|
+
colors.muted(
|
|
1633
|
+
`\u2026 (${body.length - MAX_CHARS_PER_PAGE} more chars \u2014 use /parsed ${docId} for the raw stream)`
|
|
1634
|
+
)
|
|
1635
|
+
);
|
|
1636
|
+
} else {
|
|
1637
|
+
lines.push(body);
|
|
1638
|
+
}
|
|
1639
|
+
lines.push("");
|
|
1640
|
+
}
|
|
1641
|
+
return lines;
|
|
1642
|
+
}
|
|
1643
|
+
var viewCommands = [
|
|
1644
|
+
{
|
|
1645
|
+
name: "view",
|
|
1646
|
+
description: "Read a document in the chat log, with citation hints per page",
|
|
1647
|
+
argHint: "doc-id",
|
|
1648
|
+
minArgs: 1,
|
|
1649
|
+
requires: "workspace",
|
|
1650
|
+
run: async (ctx) => {
|
|
1651
|
+
const { args, arbi, authHeaders, tui } = ctx;
|
|
1652
|
+
const docId = args[0].trim();
|
|
1653
|
+
const [docs, parsed] = await Promise.all([
|
|
1654
|
+
sdk.documents.getDocuments(arbi, [docId]),
|
|
1655
|
+
sdk.documents.getParsedContent(authHeaders, docId, "content")
|
|
1656
|
+
]);
|
|
1657
|
+
const doc = docs[0];
|
|
1658
|
+
if (!doc) return `Document ${docId} not found.`;
|
|
1659
|
+
const meta = doc.doc_metadata;
|
|
1660
|
+
const title = meta?.title ?? doc.file_name ?? docId;
|
|
1661
|
+
const content = parsed.content ?? "";
|
|
1662
|
+
if (!content) return `No parsed content available for ${docId}.`;
|
|
1663
|
+
const pages = splitIntoPages(content);
|
|
1664
|
+
const citationsByPage = tui.lastMetadata ? indexCitationsByPage(docId, sdk.resolveCitations(tui.lastMetadata)) : /* @__PURE__ */ new Map();
|
|
1665
|
+
return renderDocView({
|
|
1666
|
+
docId,
|
|
1667
|
+
title,
|
|
1668
|
+
fileName: doc.file_name ?? null,
|
|
1669
|
+
pages,
|
|
1670
|
+
citationsByPage
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
];
|
|
1675
|
+
|
|
1676
|
+
// src/commands/sources.ts
|
|
1677
|
+
function renderSourcesList(entries) {
|
|
1678
|
+
if (entries.length === 0) {
|
|
1679
|
+
return "No sources cited yet in this conversation. Ask a question first.";
|
|
1680
|
+
}
|
|
1681
|
+
const lines = [`Sources in this conversation (${entries.length}):`, ""];
|
|
1682
|
+
for (const e of entries) {
|
|
1683
|
+
const cites = e.citationNums.map((n) => `[${n}]`).join(" ");
|
|
1684
|
+
const pages = e.pages.length > 0 ? ` p.${e.pages.join(", p.")}` : "";
|
|
1685
|
+
lines.push(` ${colors.accent(cites)} ${colors.textBold(e.title)}${pages}`);
|
|
1686
|
+
lines.push(` ${colors.muted(e.docId)}`);
|
|
1687
|
+
}
|
|
1688
|
+
lines.push("");
|
|
1689
|
+
lines.push(
|
|
1690
|
+
colors.muted("Use /view <doc-id> to read a source, or /cite N for a specific passage.")
|
|
1691
|
+
);
|
|
1692
|
+
return lines;
|
|
1693
|
+
}
|
|
1694
|
+
var sourcesCommands = [
|
|
1695
|
+
{
|
|
1696
|
+
name: "sources",
|
|
1697
|
+
description: "List documents cited in this conversation",
|
|
1698
|
+
requires: "none",
|
|
1699
|
+
run: (ctx) => {
|
|
1700
|
+
const { tui } = ctx;
|
|
1701
|
+
return renderSourcesList(tui.sourcesIndex.list());
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
];
|
|
1705
|
+
var DEFAULT_LIMIT2 = 10;
|
|
1706
|
+
function formatHistoryLine(row) {
|
|
1707
|
+
const title = row.title?.trim() || colors.muted("(untitled)");
|
|
1708
|
+
const count = typeof row.message_count === "number" ? ` \xB7 ${row.message_count} msg` : "";
|
|
1709
|
+
const when = row.updated_at ? ` \xB7 ${formatRelativeTime(row.updated_at)}` : "";
|
|
1710
|
+
return ` ${colors.accent(row.external_id)} ${title}${colors.muted(count + when)}`;
|
|
1711
|
+
}
|
|
1712
|
+
function formatRelativeTime(iso, now = /* @__PURE__ */ new Date()) {
|
|
1713
|
+
const t = new Date(iso);
|
|
1714
|
+
if (Number.isNaN(t.getTime())) return iso;
|
|
1715
|
+
const seconds = Math.floor((now.getTime() - t.getTime()) / 1e3);
|
|
1716
|
+
if (seconds < 60) return "just now";
|
|
1717
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
|
1718
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
|
|
1719
|
+
if (seconds < 86400 * 2) return "yesterday";
|
|
1720
|
+
if (seconds < 86400 * 7) return `${Math.floor(seconds / 86400)}d ago`;
|
|
1721
|
+
return t.toLocaleDateString(void 0, { month: "short", day: "numeric" });
|
|
1722
|
+
}
|
|
1723
|
+
var historyCommands = [
|
|
1724
|
+
{
|
|
1725
|
+
name: "history",
|
|
1726
|
+
description: "Browse past conversations",
|
|
1727
|
+
argHint: "count|all",
|
|
1728
|
+
requires: "workspace",
|
|
1729
|
+
run: async (ctx) => {
|
|
1730
|
+
const { arbi, args } = ctx;
|
|
1731
|
+
const arg = args[0]?.trim().toLowerCase();
|
|
1732
|
+
const convs = await sdk.conversations.listConversations(arbi);
|
|
1733
|
+
if (convs.length === 0) {
|
|
1734
|
+
return "No conversations yet in this workspace. Ask a question to start one.";
|
|
1735
|
+
}
|
|
1736
|
+
const slice = arg === "all" ? convs : convs.slice(0, parseLimit(arg) ?? DEFAULT_LIMIT2);
|
|
1737
|
+
const lines = [
|
|
1738
|
+
`Conversations (${slice.length}${slice.length < convs.length ? ` of ${convs.length}` : ""}):`,
|
|
1739
|
+
""
|
|
1740
|
+
];
|
|
1741
|
+
for (const row of slice) lines.push(formatHistoryLine(row));
|
|
1742
|
+
lines.push("");
|
|
1743
|
+
lines.push(colors.muted("Use /resume <conv-id> to switch to one of these."));
|
|
1744
|
+
return lines;
|
|
1745
|
+
}
|
|
1746
|
+
},
|
|
1747
|
+
{
|
|
1748
|
+
name: "resume",
|
|
1749
|
+
description: "Switch to a past conversation by ID",
|
|
1750
|
+
argHint: "conv-id",
|
|
1751
|
+
minArgs: 1,
|
|
1752
|
+
requires: "workspace",
|
|
1753
|
+
run: async (ctx) => {
|
|
1754
|
+
const { args, arbi, tui } = ctx;
|
|
1755
|
+
const convId = args[0].trim();
|
|
1756
|
+
const data = await sdk.conversations.getConversationThreads(arbi, convId);
|
|
1757
|
+
const threadList = data.threads ?? [];
|
|
1758
|
+
const primary = threadList[0];
|
|
1759
|
+
if (!primary?.leaf_message_ext_id) {
|
|
1760
|
+
return `Conversation ${convId} not found or has no messages.`;
|
|
1761
|
+
}
|
|
1762
|
+
tui.chatLog.clearMessages();
|
|
1763
|
+
for (const msg of primary.history ?? []) {
|
|
1764
|
+
if (msg.role === "user") tui.chatLog.addUser(msg.content);
|
|
1765
|
+
else if (msg.role === "assistant") tui.chatLog.addAssistant(msg.content);
|
|
1766
|
+
}
|
|
1767
|
+
tui.chatLog.addSystem(`--- resumed conversation ${convId} ---`, "info");
|
|
1768
|
+
tui.state.conversationMessageId = primary.leaf_message_ext_id;
|
|
1769
|
+
tui.store.updateChatSession({
|
|
1770
|
+
lastMessageExtId: primary.leaf_message_ext_id,
|
|
1771
|
+
conversationExtId: convId
|
|
1772
|
+
});
|
|
1773
|
+
tui.sourcesIndex.clear();
|
|
1774
|
+
tui.lastMetadata = null;
|
|
1775
|
+
tui.requestRender();
|
|
1776
|
+
return `Resumed conversation ${convId}.`;
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
];
|
|
1780
|
+
function parseLimit(arg) {
|
|
1781
|
+
if (!arg) return void 0;
|
|
1782
|
+
const n = Number.parseInt(arg, 10);
|
|
1783
|
+
return Number.isFinite(n) && n > 0 ? n : void 0;
|
|
1784
|
+
}
|
|
1785
|
+
init_tui_helpers();
|
|
1786
|
+
var skillCommands = [
|
|
1787
|
+
{
|
|
1788
|
+
name: "skills",
|
|
1789
|
+
description: "List user-invocable skills in the active workspace",
|
|
1790
|
+
requires: "workspace",
|
|
1791
|
+
run: async (ctx) => {
|
|
1792
|
+
const { arbi, tui } = ctx;
|
|
1793
|
+
const list = await sdk.assistant.listSkills(arbi);
|
|
1794
|
+
tui.skillsCache = list;
|
|
1795
|
+
if (list.length === 0) {
|
|
1796
|
+
return [
|
|
1797
|
+
"No skills in this workspace yet.",
|
|
1798
|
+
colors.muted(
|
|
1799
|
+
"Skills are documents with wp_type=skill. Upload a SKILL.md with frontmatter (name, description, user-invocable: true) to add one."
|
|
1800
|
+
)
|
|
1801
|
+
];
|
|
1802
|
+
}
|
|
1803
|
+
const lines = [`Skills (${list.length}):`, ""];
|
|
1804
|
+
for (const s of list) {
|
|
1805
|
+
const argHint = s.arg_hint ? colors.muted(` ${s.arg_hint}`) : "";
|
|
1806
|
+
const typeBadge = s.skill_type && s.skill_type !== "markdown" ? colors.muted(` [${s.skill_type}]`) : "";
|
|
1807
|
+
lines.push(` ${colors.accent("/" + s.slug)}${argHint}${typeBadge}`);
|
|
1808
|
+
if (s.description) {
|
|
1809
|
+
lines.push(` ${colors.muted(s.description)}`);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
lines.push("");
|
|
1813
|
+
lines.push(
|
|
1814
|
+
colors.muted("Use /skill view <slug> to read a skill, /skill source <slug> for its doc-id.")
|
|
1815
|
+
);
|
|
1816
|
+
return lines;
|
|
1817
|
+
}
|
|
1818
|
+
},
|
|
1819
|
+
{
|
|
1820
|
+
name: "skill",
|
|
1821
|
+
description: "Inspect or edit a skill (view | source | edit) <slug>",
|
|
1822
|
+
argHint: "subcommand slug",
|
|
1823
|
+
minArgs: 2,
|
|
1824
|
+
requires: "workspace",
|
|
1825
|
+
run: async (ctx) => {
|
|
1826
|
+
const { args, arbi, tui } = ctx;
|
|
1827
|
+
const [sub, slug, ...rest] = args;
|
|
1828
|
+
if (sub !== "view" && sub !== "source" && sub !== "edit") {
|
|
1829
|
+
return `Unknown /skill subcommand "${sub}". Use /skill view <slug>, /skill source <slug>, or /skill edit <slug>.`;
|
|
1830
|
+
}
|
|
1831
|
+
const list = tui.skillsCache?.length ? tui.skillsCache : await sdk.assistant.listSkills(arbi, { includeHidden: true });
|
|
1832
|
+
const skill = list.find((s) => s.slug === slug || s.name === slug);
|
|
1833
|
+
if (!skill) {
|
|
1834
|
+
return `No skill matching "${slug}" in this workspace. Use /skills to list available skills.`;
|
|
1835
|
+
}
|
|
1836
|
+
if (sub === "source") {
|
|
1837
|
+
return [
|
|
1838
|
+
colors.textBold(`${skill.name} (${skill.slug})`),
|
|
1839
|
+
colors.muted(`doc-id: ${skill.doc_ext_id}`),
|
|
1840
|
+
colors.muted(`workspace: ${skill.workspace_ext_id}`),
|
|
1841
|
+
colors.muted(
|
|
1842
|
+
"Use `arbi upload --folder skills/<slug>` to replace the SKILL.md, or edit it via the web app."
|
|
1843
|
+
)
|
|
1844
|
+
];
|
|
1845
|
+
}
|
|
1846
|
+
const { authHeaders } = ctx;
|
|
1847
|
+
if (sub === "edit") {
|
|
1848
|
+
return await editSkill(tui, authHeaders, skill);
|
|
1849
|
+
}
|
|
1850
|
+
const dlRes = await sdk.documents.downloadDocument(authHeaders, skill.doc_ext_id);
|
|
1851
|
+
const body = dlRes.ok ? await dlRes.text() : "";
|
|
1852
|
+
const lines = [colors.textBold(`${skill.name} (${skill.slug})`)];
|
|
1853
|
+
if (skill.description) lines.push(colors.muted(skill.description));
|
|
1854
|
+
if (skill.arg_hint) lines.push(colors.muted(`args: ${skill.arg_hint}`));
|
|
1855
|
+
lines.push(colors.muted(`type: ${skill.skill_type} doc: ${skill.doc_ext_id}`));
|
|
1856
|
+
lines.push("");
|
|
1857
|
+
lines.push(body);
|
|
1858
|
+
return lines;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
];
|
|
1862
|
+
async function editSkill(tui, authHeaders, skill) {
|
|
1863
|
+
const dlRes = await sdk.documents.downloadDocument(authHeaders, skill.doc_ext_id);
|
|
1864
|
+
if (!dlRes.ok) {
|
|
1865
|
+
return `Failed to fetch skill body (HTTP ${dlRes.status}). Edit aborted.`;
|
|
1866
|
+
}
|
|
1867
|
+
const original = await dlRes.text();
|
|
1868
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `arbi-skill-${skill.slug}-`));
|
|
1869
|
+
const path$1 = path.join(dir, "SKILL.md");
|
|
1870
|
+
fs.writeFileSync(path$1, original, "utf8");
|
|
1871
|
+
const mtimeBefore = fs.statSync(path$1).mtimeMs;
|
|
1872
|
+
const editor = process.env.VISUAL || process.env.EDITOR || "vi";
|
|
1873
|
+
const result = await runInteractiveFlow(
|
|
1874
|
+
tui,
|
|
1875
|
+
colors.muted(`Opening ${skill.slug}/SKILL.md in ${editor}\u2026`),
|
|
1876
|
+
async () => {
|
|
1877
|
+
const child = child_process.spawnSync(editor, [path$1], { stdio: "inherit" });
|
|
1878
|
+
if (child.error) throw child.error;
|
|
1879
|
+
if (child.status !== 0 && child.status !== null) {
|
|
1880
|
+
throw new Error(`${editor} exited with status ${child.status}`);
|
|
1881
|
+
}
|
|
1882
|
+
return fs.readFileSync(path$1, "utf8");
|
|
1883
|
+
},
|
|
1884
|
+
`Skill edit (${skill.slug}) failed`
|
|
1885
|
+
);
|
|
1886
|
+
const mtimeAfter = (() => {
|
|
1887
|
+
try {
|
|
1888
|
+
return fs.statSync(path$1).mtimeMs;
|
|
1889
|
+
} catch {
|
|
1890
|
+
return mtimeBefore;
|
|
1891
|
+
}
|
|
1892
|
+
})();
|
|
1893
|
+
const updated = result ?? original;
|
|
1894
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1895
|
+
if (result === null) {
|
|
1896
|
+
return [];
|
|
1897
|
+
}
|
|
1898
|
+
if (mtimeAfter === mtimeBefore || updated === original) {
|
|
1899
|
+
return colors.muted(`No changes to ${skill.slug}/SKILL.md \u2014 skill unchanged.`);
|
|
1900
|
+
}
|
|
1901
|
+
const blob = new Blob([updated], { type: "text/markdown" });
|
|
1902
|
+
await sdk.documents.uploadFile(authHeaders, blob, "SKILL.md", {
|
|
1903
|
+
folder: `skills/${skill.slug}`,
|
|
1904
|
+
wpType: "skill"
|
|
1905
|
+
});
|
|
1906
|
+
await tui.refreshSkillsCache();
|
|
1907
|
+
return [
|
|
1908
|
+
colors.textBold(`Updated ${skill.name} (${skill.slug})`),
|
|
1909
|
+
colors.muted("Skill cache refreshed \u2014 new description and args take effect immediately.")
|
|
1910
|
+
];
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1339
1913
|
// src/commands/index.ts
|
|
1340
1914
|
function registerAllCommands() {
|
|
1341
1915
|
registerCommands(generalCommands);
|
|
@@ -1346,6 +1920,21 @@ function registerAllCommands() {
|
|
|
1346
1920
|
registerCommands(tagCommands);
|
|
1347
1921
|
registerCommands(miscCommands);
|
|
1348
1922
|
registerCommands(citationCommands);
|
|
1923
|
+
registerCommands(eventCommands);
|
|
1924
|
+
registerCommands(viewCommands);
|
|
1925
|
+
registerCommands(sourcesCommands);
|
|
1926
|
+
registerCommands(historyCommands);
|
|
1927
|
+
registerCommands(skillCommands);
|
|
1928
|
+
}
|
|
1929
|
+
function extractStepDetails(data) {
|
|
1930
|
+
const detail = data.detail;
|
|
1931
|
+
if (!detail || detail.length === 0) return void 0;
|
|
1932
|
+
const d = detail[0];
|
|
1933
|
+
if (!d) return void 0;
|
|
1934
|
+
const result = {};
|
|
1935
|
+
if (d.tool) result.tool = d.tool;
|
|
1936
|
+
if (d.message) result.args = d.message;
|
|
1937
|
+
return result.tool || result.args || result.result ? result : void 0;
|
|
1349
1938
|
}
|
|
1350
1939
|
async function streamResponse(tui, response) {
|
|
1351
1940
|
tui.chatLog.startAssistant();
|
|
@@ -1372,7 +1961,7 @@ async function streamResponse(tui, response) {
|
|
|
1372
1961
|
onAgentStep: (data) => {
|
|
1373
1962
|
const label = sdk.formatAgentStepLabel(data);
|
|
1374
1963
|
if (label) {
|
|
1375
|
-
tui.chatLog.addAgentStep(label);
|
|
1964
|
+
tui.chatLog.addAgentStep(label, extractStepDetails(data));
|
|
1376
1965
|
tui.requestRender();
|
|
1377
1966
|
}
|
|
1378
1967
|
},
|
|
@@ -1388,6 +1977,7 @@ async function streamResponse(tui, response) {
|
|
|
1388
1977
|
const result = await sdk.streamSSE(response, callbacks);
|
|
1389
1978
|
tui.chatLog.finalizeAssistant();
|
|
1390
1979
|
tui.lastMetadata = result.metadata ?? null;
|
|
1980
|
+
tui.sourcesIndex.recordCitations(result.metadata);
|
|
1391
1981
|
const summary = sdk.formatStreamSummary(result, elapsedTime);
|
|
1392
1982
|
if (summary) {
|
|
1393
1983
|
const refs = sdk.countCitations(result.metadata ?? null);
|
|
@@ -1596,43 +2186,50 @@ var DURATION_BY_LEVEL = {
|
|
|
1596
2186
|
function isNotification(msg) {
|
|
1597
2187
|
return "sender" in msg && "recipient" in msg;
|
|
1598
2188
|
}
|
|
1599
|
-
|
|
2189
|
+
function shouldToast(level, msgType) {
|
|
2190
|
+
if (level === "error" || level === "warning" || level === "success") return true;
|
|
2191
|
+
if (msgType === "response_complete" || msgType === "batch_complete") return true;
|
|
2192
|
+
return false;
|
|
2193
|
+
}
|
|
2194
|
+
async function handleMessage(msg, toasts, eventLog, tui) {
|
|
1600
2195
|
if (isNotification(msg) && msg.type === "user_message" && tui) {
|
|
1601
2196
|
const handled = await handleIncomingDm(tui, msg);
|
|
1602
2197
|
if (handled) return;
|
|
1603
2198
|
}
|
|
1604
2199
|
const { text, level } = sdk.formatWsMessage(msg);
|
|
1605
|
-
const
|
|
1606
|
-
|
|
2200
|
+
const msgType = msg.type;
|
|
2201
|
+
eventLog.add({ text, level, msgType });
|
|
2202
|
+
if (shouldToast(level, msgType)) {
|
|
2203
|
+
const duration = DURATION_BY_LEVEL[level] ?? DURATION_DEFAULT;
|
|
2204
|
+
toasts.show(text, level, duration);
|
|
2205
|
+
}
|
|
1607
2206
|
}
|
|
1608
2207
|
async function connectTuiWebSocket(options) {
|
|
1609
|
-
const { baseUrl, accessToken, toasts, tui } = options;
|
|
2208
|
+
const { baseUrl, accessToken, toasts, eventLog, tui } = options;
|
|
2209
|
+
const recordTransport = (text, level) => {
|
|
2210
|
+
eventLog.add({ text, level });
|
|
2211
|
+
const duration = DURATION_BY_LEVEL[level] ?? DURATION_DEFAULT;
|
|
2212
|
+
toasts.show(text, level, duration);
|
|
2213
|
+
};
|
|
1610
2214
|
try {
|
|
1611
2215
|
const connection = await sdk.connectWithReconnect({
|
|
1612
2216
|
baseUrl,
|
|
1613
2217
|
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
|
-
}
|
|
2218
|
+
onMessage: (msg) => handleMessage(msg, toasts, eventLog, tui ?? null),
|
|
2219
|
+
onClose: () => recordTransport("WebSocket disconnected", "warning"),
|
|
2220
|
+
onReconnecting: (attempt, maxRetries) => recordTransport(`Reconnecting... (${attempt}/${maxRetries})`, "warning"),
|
|
2221
|
+
onReconnected: () => recordTransport("WebSocket reconnected", "success"),
|
|
2222
|
+
onReconnectFailed: () => recordTransport("WebSocket reconnection failed", "error")
|
|
1627
2223
|
});
|
|
1628
2224
|
return connection;
|
|
1629
2225
|
} catch {
|
|
1630
|
-
|
|
2226
|
+
recordTransport("WebSocket connection failed", "warning");
|
|
1631
2227
|
return null;
|
|
1632
2228
|
}
|
|
1633
2229
|
}
|
|
1634
2230
|
|
|
1635
2231
|
// src/tui.ts
|
|
2232
|
+
init_tui_helpers();
|
|
1636
2233
|
var ArbiTui = class {
|
|
1637
2234
|
tui;
|
|
1638
2235
|
header;
|
|
@@ -1658,6 +2255,23 @@ var ArbiTui = class {
|
|
|
1658
2255
|
dmChannel = null;
|
|
1659
2256
|
/** Last response metadata — used by /cite for citation browsing. */
|
|
1660
2257
|
lastMetadata = null;
|
|
2258
|
+
/** Persistent log of WebSocket events for this session. Toasts are
|
|
2259
|
+
* ephemeral; this is the "what happened?" scrollback that
|
|
2260
|
+
* ``/events`` reads from. Public so ws-handler can append and the
|
|
2261
|
+
* events command can read. */
|
|
2262
|
+
eventLog = new WsEventLog();
|
|
2263
|
+
/** Running tally of documents cited in the current conversation.
|
|
2264
|
+
* Cleared on ``/new``; appended to after every assistant response
|
|
2265
|
+
* that carries citation metadata. Drives ``/sources`` and (later)
|
|
2266
|
+
* the persistent right-side sources panel. */
|
|
2267
|
+
sourcesIndex = new SourcesIndex();
|
|
2268
|
+
/** Cached list of user-invocable skills in the active workspace.
|
|
2269
|
+
* Refreshed on workspace switch (``setWorkspaceContext``) and
|
|
2270
|
+
* invalidated to ``[]`` when leaving a workspace. Drives the
|
|
2271
|
+
* autocomplete provider and the ``/skills`` / ``/skill view``
|
|
2272
|
+
* commands. ``null`` means "haven't fetched yet"; ``[]`` means
|
|
2273
|
+
* "fetched, no skills exist". */
|
|
2274
|
+
skillsCache = [];
|
|
1661
2275
|
/** Active citation overlay handle (null when not showing). */
|
|
1662
2276
|
citationOverlay = null;
|
|
1663
2277
|
/** Input listener ID for overlay dismiss (null when no overlay). */
|
|
@@ -1728,15 +2342,60 @@ var ArbiTui = class {
|
|
|
1728
2342
|
/** Create and wire a new editor instance. */
|
|
1729
2343
|
createEditor() {
|
|
1730
2344
|
const editor = new ArbiEditor(this.tui);
|
|
1731
|
-
editor.setAutocompleteProvider(
|
|
2345
|
+
editor.setAutocompleteProvider(
|
|
2346
|
+
new piTui.CombinedAutocompleteProvider(toSlashCommands(), process.cwd())
|
|
2347
|
+
);
|
|
1732
2348
|
editor.onSubmit = (text) => this.handleSubmit(text);
|
|
1733
2349
|
editor.onEscape = () => this.handleAbort();
|
|
1734
2350
|
editor.onCtrlC = () => this.shutdown();
|
|
1735
2351
|
editor.onCtrlD = () => this.shutdown();
|
|
1736
2352
|
editor.onCtrlW = () => this.handleSubmit("/workspaces");
|
|
1737
2353
|
editor.onCtrlN = () => this.handleSubmit("/new");
|
|
2354
|
+
editor.onCitationKey = (n) => this.handleSubmit(`/cite ${n}`);
|
|
2355
|
+
editor.onBufferChange = (text) => this.handleBufferChange(text);
|
|
1738
2356
|
return editor;
|
|
1739
2357
|
}
|
|
2358
|
+
/** The slug we last surfaced as a pre-flight hint. ``null`` once
|
|
2359
|
+
* the user has navigated away from the skill prefix (e.g. typed a
|
|
2360
|
+
* non-slash char first, or deleted past the slash). Tracking this
|
|
2361
|
+
* here — not in the editor — keeps the editor pure: it just emits
|
|
2362
|
+
* text, the TUI decides what's interesting. */
|
|
2363
|
+
lastHintedSlug = null;
|
|
2364
|
+
/**
|
|
2365
|
+
* Pre-flight skill hint dispatcher.
|
|
2366
|
+
*
|
|
2367
|
+
* Watches the editor buffer; when it starts with ``/<token>`` where
|
|
2368
|
+
* ``<token>`` is a slug in ``skillsCache`` *and* not also a static
|
|
2369
|
+
* command (those have their own ``--help`` style help), we drop a
|
|
2370
|
+
* one-line description into the chat log so the user sees what
|
|
2371
|
+
* they're about to invoke before pressing Enter. Equivalent of
|
|
2372
|
+
* claude-code's slash-command palette preview, minus the modal.
|
|
2373
|
+
*
|
|
2374
|
+
* Dedup rule: only emit when the matched slug *changes*. Typing
|
|
2375
|
+
* "/foo" → "/foob" → "/foo" emits once for ``foo``, never for the
|
|
2376
|
+
* intermediate ``foob`` (no match), and not again when the user
|
|
2377
|
+
* lands back on ``foo``. The state resets whenever the buffer no
|
|
2378
|
+
* longer matches anything, so a fresh ``/foo`` after a clear emits
|
|
2379
|
+
* the hint again.
|
|
2380
|
+
*/
|
|
2381
|
+
handleBufferChange(text) {
|
|
2382
|
+
const parsed = sdk.parseSlashCommand(text);
|
|
2383
|
+
if (!parsed) {
|
|
2384
|
+
this.lastHintedSlug = null;
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
const slug = parsed.slug;
|
|
2388
|
+
if (this.skillsCache.length === 0) return;
|
|
2389
|
+
const skill = this.skillsCache.find((s) => s.slug === slug);
|
|
2390
|
+
if (!skill) {
|
|
2391
|
+
this.lastHintedSlug = null;
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
if (this.lastHintedSlug === slug) return;
|
|
2395
|
+
this.lastHintedSlug = slug;
|
|
2396
|
+
const argHint = skill.arg_hint ? ` ${skill.arg_hint}` : "";
|
|
2397
|
+
showMessage(this, `Skill: /${skill.slug}${argHint} \u2014 ${skill.description}`, "info");
|
|
2398
|
+
}
|
|
1740
2399
|
/** Request a render update. */
|
|
1741
2400
|
requestRender() {
|
|
1742
2401
|
this.tui.requestRender();
|
|
@@ -1757,6 +2416,34 @@ var ArbiTui = class {
|
|
|
1757
2416
|
this.state.workspaceId = ctx.workspaceId;
|
|
1758
2417
|
this.updateHeader();
|
|
1759
2418
|
this.connectWebSocket();
|
|
2419
|
+
void this.refreshSkillsCache();
|
|
2420
|
+
}
|
|
2421
|
+
/** Refresh the cached skills list for the current workspace. Called
|
|
2422
|
+
* on workspace switch and from ``/skills`` itself so a freshly
|
|
2423
|
+
* uploaded skill becomes invocable without a TUI restart. */
|
|
2424
|
+
async refreshSkillsCache() {
|
|
2425
|
+
if (!this.workspaceContext) {
|
|
2426
|
+
this.skillsCache = [];
|
|
2427
|
+
this.rebuildAutocomplete();
|
|
2428
|
+
return;
|
|
2429
|
+
}
|
|
2430
|
+
try {
|
|
2431
|
+
this.skillsCache = await sdk.assistant.listSkills(this.workspaceContext.arbi);
|
|
2432
|
+
} catch {
|
|
2433
|
+
this.skillsCache = [];
|
|
2434
|
+
}
|
|
2435
|
+
this.rebuildAutocomplete();
|
|
2436
|
+
}
|
|
2437
|
+
/** Re-feed the editor's autocomplete provider with the merged
|
|
2438
|
+
* static-commands + skills list. Called on workspace switch and
|
|
2439
|
+
* after every ``refreshSkillsCache`` so a newly uploaded skill
|
|
2440
|
+
* appears in tab-complete without a TUI restart. */
|
|
2441
|
+
rebuildAutocomplete() {
|
|
2442
|
+
if (!this.editor) return;
|
|
2443
|
+
const merged = toSlashCommandsWithSkills(
|
|
2444
|
+
this.skillsCache.map((s) => ({ slug: s.slug, description: s.description }))
|
|
2445
|
+
);
|
|
2446
|
+
this.editor.setAutocompleteProvider(new piTui.CombinedAutocompleteProvider(merged, process.cwd()));
|
|
1760
2447
|
}
|
|
1761
2448
|
/** Refresh workspace context (after switching workspaces). */
|
|
1762
2449
|
async refreshWorkspaceContext() {
|
|
@@ -1784,6 +2471,7 @@ var ArbiTui = class {
|
|
|
1784
2471
|
baseUrl: config.baseUrl,
|
|
1785
2472
|
accessToken,
|
|
1786
2473
|
toasts: this.toastContainer,
|
|
2474
|
+
eventLog: this.eventLog,
|
|
1787
2475
|
tui: this
|
|
1788
2476
|
}).then((conn) => {
|
|
1789
2477
|
this.wsConnection = conn;
|
|
@@ -1841,6 +2529,14 @@ var ArbiTui = class {
|
|
|
1841
2529
|
previousResponseId
|
|
1842
2530
|
});
|
|
1843
2531
|
const result = await streamResponse(this, response);
|
|
2532
|
+
if (this.lastMetadata) {
|
|
2533
|
+
const n = sdk.countCitations(this.lastMetadata);
|
|
2534
|
+
if (n > 0) {
|
|
2535
|
+
const cap = Math.min(n, 9);
|
|
2536
|
+
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`;
|
|
2537
|
+
this.chatLog.addSystem(`${label} \u2014 press Alt+1..${cap} to view`, "info");
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
1844
2540
|
if (result.assistantMessageExtId) {
|
|
1845
2541
|
this.state.conversationMessageId = result.assistantMessageExtId;
|
|
1846
2542
|
const sessionUpdate = {
|