@runtypelabs/persona 3.17.0 → 3.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +142 -0
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-HPZY7oAI.d.cts → types-cwY5HaFD.d.cts} +25 -0
- package/dist/animations/{types-HPZY7oAI.d.ts → types-cwY5HaFD.d.ts} +25 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/index.cjs +47 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +300 -1
- package/dist/index.d.ts +300 -1
- package/dist/index.global.js +75 -75
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +47 -47
- package/dist/index.js.map +1 -1
- package/dist/theme-editor.cjs +1432 -159
- package/dist/theme-editor.d.cts +218 -0
- package/dist/theme-editor.d.ts +218 -0
- package/dist/theme-editor.js +1432 -159
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +14 -0
- package/dist/theme-reference.d.ts +14 -0
- package/dist/widget.css +432 -0
- package/package.json +1 -1
- package/src/client.test.ts +134 -0
- package/src/client.ts +71 -0
- package/src/components/ask-user-question-bubble.test.ts +583 -0
- package/src/components/ask-user-question-bubble.ts +924 -0
- package/src/components/messages.ts +33 -1
- package/src/components/panel.ts +41 -4
- package/src/defaults.ts +21 -0
- package/src/index.ts +16 -1
- package/src/plugins/types.ts +57 -0
- package/src/session.test.ts +183 -0
- package/src/session.ts +242 -3
- package/src/styles/widget.css +432 -0
- package/src/types/theme.ts +15 -0
- package/src/types.ts +150 -0
- package/src/ui.ask-user-question-plugin.test.ts +649 -0
- package/src/ui.ts +631 -5
- package/src/utils/storage.ts +10 -2
- package/src/utils/theme.test.ts +36 -0
- package/src/utils/tokens.ts +23 -0
package/src/ui.ts
CHANGED
|
@@ -59,6 +59,20 @@ import { MessageTransform, MessageActionCallbacks, LoadingIndicatorRenderer } fr
|
|
|
59
59
|
import { createStandardBubble, createTypingIndicator } from "./components/message-bubble";
|
|
60
60
|
import { createReasoningBubble, reasoningExpansionState, updateReasoningBubbleUI } from "./components/reasoning-bubble";
|
|
61
61
|
import { createToolBubble, toolExpansionState, updateToolBubbleUI } from "./components/tool-bubble";
|
|
62
|
+
import {
|
|
63
|
+
buildStructuredAnswers,
|
|
64
|
+
ensureAskUserQuestionSheet,
|
|
65
|
+
getCurrentIndex,
|
|
66
|
+
getQuestionCount,
|
|
67
|
+
getSelectedLabels,
|
|
68
|
+
isAskUserQuestionMessage,
|
|
69
|
+
isGroupedSheet,
|
|
70
|
+
navigateToPage,
|
|
71
|
+
parseAskUserQuestionPayload,
|
|
72
|
+
readAnswersFromSheet,
|
|
73
|
+
removeAskUserQuestionSheet,
|
|
74
|
+
setCurrentAnswer,
|
|
75
|
+
} from "./components/ask-user-question-bubble";
|
|
62
76
|
import { formatElapsedMs } from "./utils/formatting";
|
|
63
77
|
import { createApprovalBubble } from "./components/approval-bubble";
|
|
64
78
|
import { createSuggestions } from "./components/suggestions";
|
|
@@ -328,6 +342,10 @@ type Controller = {
|
|
|
328
342
|
upsertArtifact: (manual: PersonaArtifactManualUpsert) => PersonaArtifactRecord | null;
|
|
329
343
|
selectArtifact: (id: string) => void;
|
|
330
344
|
clearArtifacts: () => void;
|
|
345
|
+
/** Read current artifacts (useful on init to rebuild host-side tab state after hydration). */
|
|
346
|
+
getArtifacts: () => PersonaArtifactRecord[];
|
|
347
|
+
/** Read the currently selected artifact id (paired with `getArtifacts`). */
|
|
348
|
+
getSelectedArtifactId: () => string | null;
|
|
331
349
|
/**
|
|
332
350
|
* Focus the chat input. Returns true if focus succeeded, false if panel is closed
|
|
333
351
|
* (launcher mode) or textarea is unavailable.
|
|
@@ -517,6 +535,13 @@ export const createAgentExperience = (
|
|
|
517
535
|
if (processedState.messages?.length) {
|
|
518
536
|
config = { ...config, initialMessages: processedState.messages };
|
|
519
537
|
}
|
|
538
|
+
if (processedState.artifacts?.length) {
|
|
539
|
+
config = {
|
|
540
|
+
...config,
|
|
541
|
+
initialArtifacts: processedState.artifacts,
|
|
542
|
+
initialSelectedArtifactId: processedState.selectedArtifactId ?? null
|
|
543
|
+
};
|
|
544
|
+
}
|
|
520
545
|
}
|
|
521
546
|
} catch (error) {
|
|
522
547
|
if (typeof console !== "undefined") {
|
|
@@ -1408,6 +1433,385 @@ export const createAgentExperience = (
|
|
|
1408
1433
|
target.click();
|
|
1409
1434
|
});
|
|
1410
1435
|
|
|
1436
|
+
// --- ask_user_question sheet interaction ---
|
|
1437
|
+
// Event delegation for the answer-pill sheet that mounts in the composer
|
|
1438
|
+
// overlay. Handles pill pick (single), multi-select toggle + submit, free-
|
|
1439
|
+
// text pill expansion + submit, and dismissal. Selection becomes a regular
|
|
1440
|
+
// user message via session.sendMessage so the agent resumes on the next turn.
|
|
1441
|
+
const askUserOverlay = panelElements.composerOverlay;
|
|
1442
|
+
|
|
1443
|
+
const submitAskUserAnswer = (
|
|
1444
|
+
sheet: HTMLElement,
|
|
1445
|
+
text: string,
|
|
1446
|
+
meta: {
|
|
1447
|
+
source: "pick" | "multi" | "free-text" | "submit-all";
|
|
1448
|
+
values?: string[];
|
|
1449
|
+
structured?: Record<string, string | string[]>;
|
|
1450
|
+
}
|
|
1451
|
+
): void => {
|
|
1452
|
+
const trimmed = text.trim();
|
|
1453
|
+
if (!trimmed || !sessionRef.current) return;
|
|
1454
|
+
const toolCallId = sheet.getAttribute("data-tool-call-id") ?? "";
|
|
1455
|
+
const isFreeText = meta.source === "free-text";
|
|
1456
|
+
|
|
1457
|
+
// Dispatch before removing the sheet so listeners can still query DOM state.
|
|
1458
|
+
mount.dispatchEvent(
|
|
1459
|
+
new CustomEvent("persona:askUserQuestion:answered", {
|
|
1460
|
+
detail: {
|
|
1461
|
+
toolUseId: toolCallId,
|
|
1462
|
+
answer: trimmed,
|
|
1463
|
+
answers: meta.structured,
|
|
1464
|
+
values: meta.values ?? (meta.source === "multi" ? trimmed.split(", ") : [trimmed]),
|
|
1465
|
+
isFreeText,
|
|
1466
|
+
source: meta.source,
|
|
1467
|
+
},
|
|
1468
|
+
bubbles: true,
|
|
1469
|
+
composed: true,
|
|
1470
|
+
})
|
|
1471
|
+
);
|
|
1472
|
+
|
|
1473
|
+
removeAskUserQuestionSheet(askUserOverlay, toolCallId);
|
|
1474
|
+
|
|
1475
|
+
// Branch: LOCAL-tool pause (step_await) resumes via /resume with structured
|
|
1476
|
+
// toolOutputs; legacy path sends as a plain user message.
|
|
1477
|
+
const sourceMessage = sessionRef.current
|
|
1478
|
+
.getMessages()
|
|
1479
|
+
.find((m) => m.toolCall?.id === toolCallId);
|
|
1480
|
+
if (sourceMessage?.agentMetadata?.awaitingLocalTool) {
|
|
1481
|
+
sessionRef.current.resolveAskUserQuestion(sourceMessage, meta.structured ?? trimmed);
|
|
1482
|
+
} else {
|
|
1483
|
+
sessionRef.current.sendMessage(trimmed);
|
|
1484
|
+
}
|
|
1485
|
+
};
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* Persist in-progress grouped-question answers + page index back to the
|
|
1489
|
+
* source message so a refresh restores the user's spot.
|
|
1490
|
+
*/
|
|
1491
|
+
const persistGroupedProgress = (sheet: HTMLElement): void => {
|
|
1492
|
+
const session = sessionRef.current;
|
|
1493
|
+
if (!session) return;
|
|
1494
|
+
const toolCallId = sheet.getAttribute("data-tool-call-id") ?? "";
|
|
1495
|
+
const sourceMessage = session.getMessages().find((m) => m.toolCall?.id === toolCallId);
|
|
1496
|
+
if (!sourceMessage) return;
|
|
1497
|
+
session.persistAskUserQuestionProgress(sourceMessage, {
|
|
1498
|
+
answers: buildStructuredAnswers(sheet, sourceMessage),
|
|
1499
|
+
currentIndex: getCurrentIndex(sheet),
|
|
1500
|
+
});
|
|
1501
|
+
};
|
|
1502
|
+
|
|
1503
|
+
/**
|
|
1504
|
+
* Build a one-line summary string for the legacy `answer` field on the
|
|
1505
|
+
* answered event when submit-all fires from a grouped sheet.
|
|
1506
|
+
*/
|
|
1507
|
+
const stringifyStructured = (answers: Record<string, string | string[]>): string => {
|
|
1508
|
+
return Object.entries(answers)
|
|
1509
|
+
.map(([q, v]) => `${q}: ${Array.isArray(v) ? v.join(", ") : v}`)
|
|
1510
|
+
.join(" | ");
|
|
1511
|
+
};
|
|
1512
|
+
|
|
1513
|
+
/**
|
|
1514
|
+
* If `groupedAutoAdvance` is enabled (default) and we're not on the final
|
|
1515
|
+
* page, advance one step. The final page never auto-submits — users always
|
|
1516
|
+
* confirm with an explicit Submit-all click so they can review.
|
|
1517
|
+
*/
|
|
1518
|
+
const maybeAutoAdvance = (sheet: HTMLElement): void => {
|
|
1519
|
+
if (config.features?.askUserQuestion?.groupedAutoAdvance === false) return;
|
|
1520
|
+
const idx = getCurrentIndex(sheet);
|
|
1521
|
+
const count = getQuestionCount(sheet);
|
|
1522
|
+
if (idx >= count - 1) return;
|
|
1523
|
+
const sourceMessage = sessionRef.current
|
|
1524
|
+
?.getMessages()
|
|
1525
|
+
.find((m) => m.toolCall?.id === sheet.getAttribute("data-tool-call-id"));
|
|
1526
|
+
if (!sourceMessage) return;
|
|
1527
|
+
navigateToPage(sheet, sourceMessage, config, idx + 1);
|
|
1528
|
+
persistGroupedProgress(sheet);
|
|
1529
|
+
};
|
|
1530
|
+
|
|
1531
|
+
askUserOverlay.addEventListener("click", (event) => {
|
|
1532
|
+
const target = event.target as HTMLElement;
|
|
1533
|
+
const trigger = target.closest<HTMLElement>("[data-ask-user-action]");
|
|
1534
|
+
if (!trigger) return;
|
|
1535
|
+
const sheet = trigger.closest<HTMLElement>("[data-persona-ask-sheet-for]");
|
|
1536
|
+
if (!sheet) return;
|
|
1537
|
+
|
|
1538
|
+
const action = trigger.getAttribute("data-ask-user-action");
|
|
1539
|
+
event.preventDefault();
|
|
1540
|
+
event.stopPropagation();
|
|
1541
|
+
|
|
1542
|
+
if (action === "dismiss") {
|
|
1543
|
+
const toolCallId = sheet.getAttribute("data-tool-call-id") ?? "";
|
|
1544
|
+
mount.dispatchEvent(
|
|
1545
|
+
new CustomEvent("persona:askUserQuestion:dismissed", {
|
|
1546
|
+
detail: { toolUseId: toolCallId },
|
|
1547
|
+
bubbles: true,
|
|
1548
|
+
composed: true,
|
|
1549
|
+
})
|
|
1550
|
+
);
|
|
1551
|
+
removeAskUserQuestionSheet(askUserOverlay, toolCallId);
|
|
1552
|
+
|
|
1553
|
+
// Best-effort: if this sheet corresponds to a LOCAL-awaiting tool,
|
|
1554
|
+
// unblock the paused execution with a sentinel answer so the server
|
|
1555
|
+
// doesn't sit in waiting_for_local forever. Fire-and-forget — errors
|
|
1556
|
+
// are surfaced to the onError callback. Flip the answered flag first
|
|
1557
|
+
// so a racing render pass doesn't re-mount the sheet mid-dismissal.
|
|
1558
|
+
const sourceMessage = sessionRef.current
|
|
1559
|
+
?.getMessages()
|
|
1560
|
+
.find((m) => m.toolCall?.id === toolCallId);
|
|
1561
|
+
if (sourceMessage?.agentMetadata?.awaitingLocalTool) {
|
|
1562
|
+
sessionRef.current?.markAskUserQuestionResolved(sourceMessage);
|
|
1563
|
+
sessionRef.current?.resolveAskUserQuestion(sourceMessage, "(dismissed)");
|
|
1564
|
+
}
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
if (action === "pick") {
|
|
1569
|
+
const label = trigger.getAttribute("data-option-label");
|
|
1570
|
+
if (!label) return;
|
|
1571
|
+
const multiSelect = sheet.getAttribute("data-multi-select") === "true";
|
|
1572
|
+
const grouped = isGroupedSheet(sheet);
|
|
1573
|
+
|
|
1574
|
+
if (grouped && multiSelect) {
|
|
1575
|
+
const stored = readAnswersFromSheet(sheet)[getCurrentIndex(sheet)];
|
|
1576
|
+
const set = new Set<string>(Array.isArray(stored) ? stored : []);
|
|
1577
|
+
if (set.has(label)) set.delete(label);
|
|
1578
|
+
else set.add(label);
|
|
1579
|
+
setCurrentAnswer(sheet, Array.from(set));
|
|
1580
|
+
persistGroupedProgress(sheet);
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
if (grouped) {
|
|
1585
|
+
setCurrentAnswer(sheet, label);
|
|
1586
|
+
persistGroupedProgress(sheet);
|
|
1587
|
+
maybeAutoAdvance(sheet);
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// 1-question modes — preserve original UX.
|
|
1592
|
+
if (multiSelect) {
|
|
1593
|
+
const pressed = trigger.getAttribute("aria-pressed") === "true";
|
|
1594
|
+
trigger.setAttribute("aria-pressed", pressed ? "false" : "true");
|
|
1595
|
+
trigger.classList.toggle("persona-ask-pill-selected", !pressed);
|
|
1596
|
+
const submitBtn = sheet.querySelector<HTMLButtonElement>(
|
|
1597
|
+
'[data-ask-user-action="submit-multi"]'
|
|
1598
|
+
);
|
|
1599
|
+
if (submitBtn) {
|
|
1600
|
+
submitBtn.disabled = getSelectedLabels(sheet).length === 0;
|
|
1601
|
+
}
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
submitAskUserAnswer(sheet, label, { source: "pick", values: [label] });
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
if (action === "submit-multi") {
|
|
1609
|
+
const labels = getSelectedLabels(sheet);
|
|
1610
|
+
if (labels.length === 0) return;
|
|
1611
|
+
submitAskUserAnswer(sheet, labels.join(", "), {
|
|
1612
|
+
source: "multi",
|
|
1613
|
+
values: labels,
|
|
1614
|
+
});
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
if (action === "open-free-text") {
|
|
1619
|
+
const row = sheet.querySelector<HTMLElement>('[data-ask-free-text-row="true"]');
|
|
1620
|
+
if (row) {
|
|
1621
|
+
row.classList.remove("persona-hidden");
|
|
1622
|
+
const input = row.querySelector<HTMLInputElement>('[data-ask-free-text-input="true"]');
|
|
1623
|
+
input?.focus();
|
|
1624
|
+
}
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
if (action === "focus-free-text") {
|
|
1629
|
+
// Rows-layout Other row: input lives inside the row container itself.
|
|
1630
|
+
// Native click on the input already focuses it; this branch handles
|
|
1631
|
+
// clicks on the badge or row chrome AND digit-shortcut activations.
|
|
1632
|
+
const input = sheet.querySelector<HTMLInputElement>('[data-ask-free-text-input="true"]');
|
|
1633
|
+
input?.focus();
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
if (action === "submit-free-text") {
|
|
1638
|
+
const input = sheet.querySelector<HTMLInputElement>('[data-ask-free-text-input="true"]');
|
|
1639
|
+
const text = input?.value ?? "";
|
|
1640
|
+
if (!text.trim()) return;
|
|
1641
|
+
if (isGroupedSheet(sheet)) {
|
|
1642
|
+
setCurrentAnswer(sheet, text.trim());
|
|
1643
|
+
persistGroupedProgress(sheet);
|
|
1644
|
+
maybeAutoAdvance(sheet);
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
submitAskUserAnswer(sheet, text, { source: "free-text" });
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
if (action === "next" || action === "back") {
|
|
1652
|
+
if (!sessionRef.current) return;
|
|
1653
|
+
const toolCallId = sheet.getAttribute("data-tool-call-id") ?? "";
|
|
1654
|
+
const sourceMessage = sessionRef.current
|
|
1655
|
+
.getMessages()
|
|
1656
|
+
.find((m) => m.toolCall?.id === toolCallId);
|
|
1657
|
+
if (!sourceMessage) return;
|
|
1658
|
+
// Flush any unsubmitted free-text input as the current answer.
|
|
1659
|
+
const freeInput = sheet.querySelector<HTMLInputElement>('[data-ask-free-text-input="true"]');
|
|
1660
|
+
const pending = freeInput?.value?.trim() ?? "";
|
|
1661
|
+
if (pending) {
|
|
1662
|
+
const stored = readAnswersFromSheet(sheet)[getCurrentIndex(sheet)];
|
|
1663
|
+
if (typeof stored !== "string" || stored !== pending) {
|
|
1664
|
+
setCurrentAnswer(sheet, pending);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
const direction = action === "next" ? 1 : -1;
|
|
1668
|
+
const nextIdx = getCurrentIndex(sheet) + direction;
|
|
1669
|
+
navigateToPage(sheet, sourceMessage, config, nextIdx);
|
|
1670
|
+
persistGroupedProgress(sheet);
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
if (action === "submit-all") {
|
|
1675
|
+
if (!sessionRef.current) return;
|
|
1676
|
+
const toolCallId = sheet.getAttribute("data-tool-call-id") ?? "";
|
|
1677
|
+
const sourceMessage = sessionRef.current
|
|
1678
|
+
.getMessages()
|
|
1679
|
+
.find((m) => m.toolCall?.id === toolCallId);
|
|
1680
|
+
if (!sourceMessage) return;
|
|
1681
|
+
// Flush any pending free-text on the final page first.
|
|
1682
|
+
const freeInput = sheet.querySelector<HTMLInputElement>('[data-ask-free-text-input="true"]');
|
|
1683
|
+
const pending = freeInput?.value?.trim() ?? "";
|
|
1684
|
+
if (pending) setCurrentAnswer(sheet, pending);
|
|
1685
|
+
|
|
1686
|
+
const structured = buildStructuredAnswers(sheet, sourceMessage);
|
|
1687
|
+
// Persist final answers to message metadata BEFORE resolving so the
|
|
1688
|
+
// answered-state review card (which reads `agentMetadata
|
|
1689
|
+
// .askUserQuestionAnswers`) shows the user's actual picks instead of
|
|
1690
|
+
// "(skipped)" placeholders. Without this, any answer set only via the
|
|
1691
|
+
// pending-flush above (or via paths that bypassed the per-pick persist
|
|
1692
|
+
// hook) would be missing from the transcript review even though it
|
|
1693
|
+
// landed in the structured payload sent to the agent.
|
|
1694
|
+
sessionRef.current.persistAskUserQuestionProgress(sourceMessage, {
|
|
1695
|
+
answers: structured,
|
|
1696
|
+
currentIndex: getCurrentIndex(sheet),
|
|
1697
|
+
});
|
|
1698
|
+
const summary = stringifyStructured(structured);
|
|
1699
|
+
submitAskUserAnswer(sheet, summary || "(submitted)", {
|
|
1700
|
+
source: "submit-all",
|
|
1701
|
+
structured,
|
|
1702
|
+
});
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
if (action === "skip") {
|
|
1707
|
+
if (!sessionRef.current) return;
|
|
1708
|
+
const toolCallId = sheet.getAttribute("data-tool-call-id") ?? "";
|
|
1709
|
+
const sourceMessage = sessionRef.current
|
|
1710
|
+
.getMessages()
|
|
1711
|
+
.find((m) => m.toolCall?.id === toolCallId);
|
|
1712
|
+
if (!sourceMessage) return;
|
|
1713
|
+
|
|
1714
|
+
const grouped = isGroupedSheet(sheet);
|
|
1715
|
+
const idx = getCurrentIndex(sheet);
|
|
1716
|
+
const count = getQuestionCount(sheet);
|
|
1717
|
+
const isFinal = idx >= count - 1;
|
|
1718
|
+
|
|
1719
|
+
// Single-question payloads behave like dismiss.
|
|
1720
|
+
if (!grouped) {
|
|
1721
|
+
mount.dispatchEvent(
|
|
1722
|
+
new CustomEvent("persona:askUserQuestion:dismissed", {
|
|
1723
|
+
detail: { toolUseId: toolCallId },
|
|
1724
|
+
bubbles: true,
|
|
1725
|
+
composed: true,
|
|
1726
|
+
})
|
|
1727
|
+
);
|
|
1728
|
+
removeAskUserQuestionSheet(askUserOverlay, toolCallId);
|
|
1729
|
+
if (sourceMessage.agentMetadata?.awaitingLocalTool) {
|
|
1730
|
+
sessionRef.current.markAskUserQuestionResolved(sourceMessage);
|
|
1731
|
+
sessionRef.current.resolveAskUserQuestion(sourceMessage, "(dismissed)");
|
|
1732
|
+
}
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// Drop the current question's answer (if any) so it's absent from the
|
|
1737
|
+
// resolved Record. setCurrentAnswer with an empty string deletes the
|
|
1738
|
+
// index from the in-memory map.
|
|
1739
|
+
setCurrentAnswer(sheet, "");
|
|
1740
|
+
// Also clear any unsubmitted free-text on this page.
|
|
1741
|
+
const freeInput = sheet.querySelector<HTMLInputElement>('[data-ask-free-text-input="true"]');
|
|
1742
|
+
if (freeInput) freeInput.value = "";
|
|
1743
|
+
|
|
1744
|
+
if (isFinal) {
|
|
1745
|
+
// Submit with whatever has been recorded so far.
|
|
1746
|
+
const structured = buildStructuredAnswers(sheet, sourceMessage);
|
|
1747
|
+
const summary = stringifyStructured(structured);
|
|
1748
|
+
submitAskUserAnswer(sheet, summary || "(skipped)", {
|
|
1749
|
+
source: "submit-all",
|
|
1750
|
+
structured,
|
|
1751
|
+
});
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// Intermediate page: advance one step without recording.
|
|
1756
|
+
navigateToPage(sheet, sourceMessage, config, idx + 1);
|
|
1757
|
+
persistGroupedProgress(sheet);
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
});
|
|
1761
|
+
|
|
1762
|
+
// Enter on the free-text input → submit. Stays on the overlay because the
|
|
1763
|
+
// event target IS the input, which lives inside the overlay subtree.
|
|
1764
|
+
askUserOverlay.addEventListener("keydown", (event) => {
|
|
1765
|
+
if (event.key !== "Enter") return;
|
|
1766
|
+
const target = event.target as HTMLElement;
|
|
1767
|
+
const input = target as HTMLInputElement;
|
|
1768
|
+
if (!input.matches?.('[data-ask-free-text-input="true"]')) return;
|
|
1769
|
+
const sheet = input.closest<HTMLElement>("[data-persona-ask-sheet-for]");
|
|
1770
|
+
if (!sheet) return;
|
|
1771
|
+
event.preventDefault();
|
|
1772
|
+
const text = input.value;
|
|
1773
|
+
if (!text.trim()) return;
|
|
1774
|
+
if (isGroupedSheet(sheet)) {
|
|
1775
|
+
setCurrentAnswer(sheet, text.trim());
|
|
1776
|
+
persistGroupedProgress(sheet);
|
|
1777
|
+
maybeAutoAdvance(sheet);
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
submitAskUserAnswer(sheet, text, { source: "free-text" });
|
|
1781
|
+
});
|
|
1782
|
+
|
|
1783
|
+
// Digit 1–9 → pick option N on the current rows-layout single-select page.
|
|
1784
|
+
// Listens on `document` so the shortcut fires regardless of where focus
|
|
1785
|
+
// currently sits (host page body, panel chrome, anywhere). The handler
|
|
1786
|
+
// gates strictly: only fires when an active sheet is mounted in our
|
|
1787
|
+
// overlay, and bails when focus is on any input/textarea/contenteditable
|
|
1788
|
+
// (covers the free-text input, the chat composer, and any host-page input).
|
|
1789
|
+
const handleAskUserDigitKey = (event: KeyboardEvent): void => {
|
|
1790
|
+
if (!/^[1-9]$/.test(event.key)) return;
|
|
1791
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
1792
|
+
const target = event.target as HTMLElement | null;
|
|
1793
|
+
if (
|
|
1794
|
+
target?.tagName === "INPUT" ||
|
|
1795
|
+
target?.tagName === "TEXTAREA" ||
|
|
1796
|
+
target?.isContentEditable
|
|
1797
|
+
) {
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
const sheet = askUserOverlay.querySelector<HTMLElement>("[data-persona-ask-sheet-for]");
|
|
1801
|
+
if (!sheet) return;
|
|
1802
|
+
if (sheet.getAttribute("data-ask-layout") !== "rows") return;
|
|
1803
|
+
if (sheet.getAttribute("data-multi-select") === "true") return;
|
|
1804
|
+
const n = Number(event.key);
|
|
1805
|
+
const pills = sheet.querySelectorAll<HTMLElement>(
|
|
1806
|
+
'[data-ask-pill-list="true"] [data-ask-user-action="pick"], [data-ask-pill-list="true"] [data-ask-user-action="focus-free-text"]'
|
|
1807
|
+
);
|
|
1808
|
+
const target_pill = pills[n - 1];
|
|
1809
|
+
if (!target_pill) return;
|
|
1810
|
+
event.preventDefault();
|
|
1811
|
+
target_pill.click();
|
|
1812
|
+
};
|
|
1813
|
+
document.addEventListener("keydown", handleAskUserDigitKey);
|
|
1814
|
+
|
|
1411
1815
|
let artifactSplitRoot: HTMLElement | null = null;
|
|
1412
1816
|
let artifactResizeHandle: HTMLElement | null = null;
|
|
1413
1817
|
let artifactResizeUnbind: (() => void) | null = null;
|
|
@@ -1965,6 +2369,10 @@ export const createAgentExperience = (
|
|
|
1965
2369
|
applyArtifactPaneAppearance(mount, config);
|
|
1966
2370
|
|
|
1967
2371
|
const destroyCallbacks: Array<() => void> = [];
|
|
2372
|
+
// Clean up the document-level digit-key shortcut listener registered earlier.
|
|
2373
|
+
destroyCallbacks.push(() => {
|
|
2374
|
+
document.removeEventListener("keydown", handleAskUserDigitKey);
|
|
2375
|
+
});
|
|
1968
2376
|
|
|
1969
2377
|
let teardownHostStacking: (() => void) | null = null;
|
|
1970
2378
|
let releaseScrollLock: (() => void) | null = null;
|
|
@@ -2056,6 +2464,10 @@ export const createAgentExperience = (
|
|
|
2056
2464
|
let session: AgentWidgetSession;
|
|
2057
2465
|
let isStreaming = false;
|
|
2058
2466
|
const messageCache = createMessageCache();
|
|
2467
|
+
// Tracks the last fingerprint we rendered a plugin-rendered ask_user_question
|
|
2468
|
+
// bubble for, per message id. Lets us skip unnecessary rebuilds across
|
|
2469
|
+
// re-renders so user state inside the plugin (typed text, focus) survives.
|
|
2470
|
+
const lastAskBubbleFingerprint = new Map<string, string>();
|
|
2059
2471
|
let configVersion = 0;
|
|
2060
2472
|
const autoFollow = createFollowStateController();
|
|
2061
2473
|
let lastScrollTop = 0;
|
|
@@ -2139,7 +2551,9 @@ export const createAgentExperience = (
|
|
|
2139
2551
|
|
|
2140
2552
|
const payload = {
|
|
2141
2553
|
messages,
|
|
2142
|
-
metadata: persistentMetadata
|
|
2554
|
+
metadata: persistentMetadata,
|
|
2555
|
+
artifacts: lastArtifactsState.artifacts,
|
|
2556
|
+
selectedArtifactId: lastArtifactsState.selectedId
|
|
2143
2557
|
};
|
|
2144
2558
|
try {
|
|
2145
2559
|
const result = storageAdapter.save(payload);
|
|
@@ -2398,15 +2812,64 @@ export const createAgentExperience = (
|
|
|
2398
2812
|
|
|
2399
2813
|
// Track active message IDs for cache pruning
|
|
2400
2814
|
const activeMessageIds = new Set<string>();
|
|
2815
|
+
// Track ask_user_question tool-call ids whose bubbles were rendered this
|
|
2816
|
+
// pass — used to prune stale sheets from the composer overlay afterward.
|
|
2817
|
+
const liveAskToolIds = new Set<string>();
|
|
2818
|
+
|
|
2819
|
+
// Plugins that render `ask_user_question` typically attach DOM listeners
|
|
2820
|
+
// directly to their buttons. The wrapper cache uses `cloneNode(true)` and
|
|
2821
|
+
// idiomorph inserts new nodes via `document.importNode` — both strip
|
|
2822
|
+
// listeners. For plugin-handled ask messages we therefore append an empty
|
|
2823
|
+
// stub during the morph pass and hydrate the live plugin bubble into the
|
|
2824
|
+
// morphed wrapper afterward (see post-morph loop below). The stub carries
|
|
2825
|
+
// `data-preserve-runtime` so subsequent passes leave the live wrapper
|
|
2826
|
+
// (with its listener-bearing bubble) untouched.
|
|
2827
|
+
const hasAskPlugin = plugins.some((p) => p.renderAskUserQuestion);
|
|
2828
|
+
type AskPluginHydrate = {
|
|
2829
|
+
messageId: string;
|
|
2830
|
+
fingerprint: string;
|
|
2831
|
+
bubble: HTMLElement | null;
|
|
2832
|
+
};
|
|
2833
|
+
const askPluginHydrate: AskPluginHydrate[] = [];
|
|
2401
2834
|
|
|
2402
2835
|
messages.forEach((message) => {
|
|
2403
2836
|
activeMessageIds.add(message.id);
|
|
2404
2837
|
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2838
|
+
const askWithPlugin = hasAskPlugin && isAskUserQuestionMessage(message);
|
|
2839
|
+
|
|
2840
|
+
// Fingerprint cache: skip re-rendering unchanged messages. Append the
|
|
2841
|
+
// ask-user-question answered/answers state so flipping `askUserQuestionAnswered`
|
|
2842
|
+
// (or accumulating answers) busts both the wrapper cache and the plugin's
|
|
2843
|
+
// `lastAskBubbleFingerprint` check, forcing a re-render of the review UX.
|
|
2844
|
+
const askMeta = isAskUserQuestionMessage(message)
|
|
2845
|
+
? `:${message.agentMetadata?.askUserQuestionAnswered ? "a" : "u"}:${
|
|
2846
|
+
message.agentMetadata?.askUserQuestionAnswers
|
|
2847
|
+
? Object.keys(message.agentMetadata.askUserQuestionAnswers).length
|
|
2848
|
+
: 0
|
|
2849
|
+
}`
|
|
2850
|
+
: "";
|
|
2851
|
+
const fingerprint = computeMessageFingerprint(message, configVersion) + askMeta;
|
|
2852
|
+
const cachedWrapper = askWithPlugin
|
|
2853
|
+
? null
|
|
2854
|
+
: getCachedWrapper(messageCache, message.id, fingerprint);
|
|
2408
2855
|
if (cachedWrapper) {
|
|
2409
2856
|
tempContainer.appendChild(cachedWrapper.cloneNode(true));
|
|
2857
|
+
// Keep the overlay sheet alive only while the server is actively
|
|
2858
|
+
// waiting on the user (awaitingLocalTool === true). Before step_await
|
|
2859
|
+
// fires, or after the answer resumes the flow, omit from
|
|
2860
|
+
// liveAskToolIds so the prune loop below removes any stale DOM sheet.
|
|
2861
|
+
// Guards against lingering skeleton sheets from tool_start events
|
|
2862
|
+
// that never get a matching step_await (e.g. LLM-hallucinated trailing
|
|
2863
|
+
// ask_user_question calls at end-of-turn).
|
|
2864
|
+
if (
|
|
2865
|
+
isAskUserQuestionMessage(message) &&
|
|
2866
|
+
message.toolCall?.id &&
|
|
2867
|
+
message.agentMetadata?.awaitingLocalTool === true &&
|
|
2868
|
+
!message.agentMetadata?.askUserQuestionAnswered
|
|
2869
|
+
) {
|
|
2870
|
+
liveAskToolIds.add(message.toolCall.id);
|
|
2871
|
+
ensureAskUserQuestionSheet(message, config, panelElements.composerOverlay);
|
|
2872
|
+
}
|
|
2410
2873
|
return;
|
|
2411
2874
|
}
|
|
2412
2875
|
|
|
@@ -2432,7 +2895,111 @@ export const createAgentExperience = (
|
|
|
2432
2895
|
// Get message layout config
|
|
2433
2896
|
const messageLayoutConfig = config.layout?.messages;
|
|
2434
2897
|
|
|
2435
|
-
|
|
2898
|
+
// ask_user_question has two rendering modes while waiting for an answer:
|
|
2899
|
+
// 1. Plugin `renderAskUserQuestion` — returns an inline transcript
|
|
2900
|
+
// element with its own UI; the composer-overlay sheet is suppressed.
|
|
2901
|
+
// 2. Built-in composer-overlay answer-pill sheet — no transcript stub.
|
|
2902
|
+
// Plugins win when they return a non-null element; otherwise fall
|
|
2903
|
+
// through to the built-in overlay.
|
|
2904
|
+
//
|
|
2905
|
+
// Once answered, the original tool message is suppressed entirely from
|
|
2906
|
+
// the transcript. `session.resolveAskUserQuestion` injects one assistant
|
|
2907
|
+
// bubble per question and one user bubble per answer (skipped questions
|
|
2908
|
+
// become an italic `*Skipped*` user bubble), so the transcript reads
|
|
2909
|
+
// like a normal Q→A conversation. Plugins do not render the answered
|
|
2910
|
+
// state.
|
|
2911
|
+
if (
|
|
2912
|
+
isAskUserQuestionMessage(message) &&
|
|
2913
|
+
message.agentMetadata?.askUserQuestionAnswered === true
|
|
2914
|
+
) {
|
|
2915
|
+
// Drop any previously-mounted plugin bubble so the morph pass
|
|
2916
|
+
// removes the now-stale interactive sheet.
|
|
2917
|
+
lastAskBubbleFingerprint.delete(message.id);
|
|
2918
|
+
const existing = container.querySelector<HTMLElement>(`#wrapper-${message.id}`);
|
|
2919
|
+
existing?.removeAttribute("data-preserve-runtime");
|
|
2920
|
+
return;
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
if (
|
|
2924
|
+
isAskUserQuestionMessage(message) &&
|
|
2925
|
+
config.features?.askUserQuestion?.enabled !== false
|
|
2926
|
+
) {
|
|
2927
|
+
const askPlugin = plugins.find((p) => typeof p.renderAskUserQuestion === "function");
|
|
2928
|
+
if (askPlugin && sessionRef.current) {
|
|
2929
|
+
const lastFp = lastAskBubbleFingerprint.get(message.id);
|
|
2930
|
+
// Whether to actually call the plugin renderer this pass. We do it
|
|
2931
|
+
// on first sight of this message, or when its fingerprint changed
|
|
2932
|
+
// (e.g. payload streamed in more options). Otherwise we rely on the
|
|
2933
|
+
// already-mounted bubble in `container`.
|
|
2934
|
+
const needsRebuild = lastFp !== fingerprint;
|
|
2935
|
+
|
|
2936
|
+
let pluginBubble: HTMLElement | null = null;
|
|
2937
|
+
if (needsRebuild) {
|
|
2938
|
+
const { payload, complete } = parseAskUserQuestionPayload(message);
|
|
2939
|
+
const messageId = message.id;
|
|
2940
|
+
const liveMessage = (): AgentWidgetMessage | undefined =>
|
|
2941
|
+
sessionRef.current?.getMessages().find((m) => m.id === messageId);
|
|
2942
|
+
pluginBubble = askPlugin.renderAskUserQuestion!({
|
|
2943
|
+
message,
|
|
2944
|
+
payload,
|
|
2945
|
+
complete,
|
|
2946
|
+
resolve: (answer) => {
|
|
2947
|
+
const live = liveMessage();
|
|
2948
|
+
if (live) sessionRef.current?.resolveAskUserQuestion(live, answer);
|
|
2949
|
+
},
|
|
2950
|
+
dismiss: () => {
|
|
2951
|
+
const live = liveMessage();
|
|
2952
|
+
if (live?.agentMetadata?.awaitingLocalTool) {
|
|
2953
|
+
sessionRef.current?.markAskUserQuestionResolved(live);
|
|
2954
|
+
sessionRef.current?.resolveAskUserQuestion(live, "(dismissed)");
|
|
2955
|
+
}
|
|
2956
|
+
},
|
|
2957
|
+
config,
|
|
2958
|
+
});
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
// If the plugin opted out (returned null on a fresh build) AND we
|
|
2962
|
+
// have no previously-mounted bubble for this message, fall back to
|
|
2963
|
+
// the built-in overlay sheet. If we already have a mounted bubble
|
|
2964
|
+
// and the plugin didn't run this pass (cached), keep using it.
|
|
2965
|
+
const previouslyMounted = lastFp != null;
|
|
2966
|
+
if (needsRebuild && pluginBubble === null && !previouslyMounted) {
|
|
2967
|
+
if (
|
|
2968
|
+
message.agentMetadata?.awaitingLocalTool === true &&
|
|
2969
|
+
!message.agentMetadata?.askUserQuestionAnswered
|
|
2970
|
+
) {
|
|
2971
|
+
liveAskToolIds.add(message.toolCall!.id);
|
|
2972
|
+
ensureAskUserQuestionSheet(message, config, panelElements.composerOverlay);
|
|
2973
|
+
}
|
|
2974
|
+
return;
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
// Append a stub wrapper for the morph pass; hydrate the real bubble
|
|
2978
|
+
// into it post-morph so its event listeners survive.
|
|
2979
|
+
const stub = document.createElement("div");
|
|
2980
|
+
stub.className = "persona-flex";
|
|
2981
|
+
stub.id = `wrapper-${message.id}`;
|
|
2982
|
+
stub.setAttribute("data-wrapper-id", message.id);
|
|
2983
|
+
stub.setAttribute("data-ask-plugin-stub", "true");
|
|
2984
|
+
stub.setAttribute("data-preserve-runtime", "true");
|
|
2985
|
+
tempContainer.appendChild(stub);
|
|
2986
|
+
askPluginHydrate.push({
|
|
2987
|
+
messageId: message.id,
|
|
2988
|
+
fingerprint,
|
|
2989
|
+
bubble: pluginBubble,
|
|
2990
|
+
});
|
|
2991
|
+
return;
|
|
2992
|
+
} else {
|
|
2993
|
+
if (
|
|
2994
|
+
message.agentMetadata?.awaitingLocalTool === true &&
|
|
2995
|
+
!message.agentMetadata?.askUserQuestionAnswered
|
|
2996
|
+
) {
|
|
2997
|
+
liveAskToolIds.add(message.toolCall!.id);
|
|
2998
|
+
ensureAskUserQuestionSheet(message, config, panelElements.composerOverlay);
|
|
2999
|
+
}
|
|
3000
|
+
return;
|
|
3001
|
+
}
|
|
3002
|
+
} else if (matchingPlugin) {
|
|
2436
3003
|
if (message.variant === "reasoning" && message.reasoning && matchingPlugin.renderReasoning) {
|
|
2437
3004
|
if (!showReasoning) return;
|
|
2438
3005
|
bubble = matchingPlugin.renderReasoning({
|
|
@@ -2610,6 +3177,20 @@ export const createAgentExperience = (
|
|
|
2610
3177
|
tempContainer.appendChild(wrapper);
|
|
2611
3178
|
});
|
|
2612
3179
|
|
|
3180
|
+
// Prune any ask_user_question sheets whose source message is no longer in
|
|
3181
|
+
// the message list (e.g. after clearChat or a splice).
|
|
3182
|
+
if (panelElements.composerOverlay) {
|
|
3183
|
+
const sheets = panelElements.composerOverlay.querySelectorAll<HTMLElement>(
|
|
3184
|
+
"[data-persona-ask-sheet-for]"
|
|
3185
|
+
);
|
|
3186
|
+
sheets.forEach((sheet) => {
|
|
3187
|
+
const id = sheet.getAttribute("data-persona-ask-sheet-for");
|
|
3188
|
+
if (id && !liveAskToolIds.has(id)) {
|
|
3189
|
+
removeAskUserQuestionSheet(panelElements.composerOverlay, id);
|
|
3190
|
+
}
|
|
3191
|
+
});
|
|
3192
|
+
}
|
|
3193
|
+
|
|
2613
3194
|
if (config.features?.toolCallDisplay?.grouped) {
|
|
2614
3195
|
const toolGroups: AgentWidgetMessage[][] = [];
|
|
2615
3196
|
let currentGroup: AgentWidgetMessage[] = [];
|
|
@@ -2839,6 +3420,35 @@ export const createAgentExperience = (
|
|
|
2839
3420
|
|
|
2840
3421
|
// Use idiomorph to morph the container contents
|
|
2841
3422
|
morphMessages(container, tempContainer);
|
|
3423
|
+
|
|
3424
|
+
// Hydrate plugin-rendered ask-question bubbles into their stub wrappers.
|
|
3425
|
+
// Idiomorph imports new nodes via `document.importNode`, which strips
|
|
3426
|
+
// listeners — so we built only an empty stub during morph and now inject
|
|
3427
|
+
// the real, listener-bearing bubble directly into the live DOM.
|
|
3428
|
+
if (askPluginHydrate.length > 0) {
|
|
3429
|
+
for (const { messageId, fingerprint, bubble } of askPluginHydrate) {
|
|
3430
|
+
const wrapper = container.querySelector(`#wrapper-${messageId}`);
|
|
3431
|
+
if (!wrapper) continue;
|
|
3432
|
+
if (bubble === null) {
|
|
3433
|
+
// No fresh bubble built this pass — either the plugin opted out
|
|
3434
|
+
// and a previously-mounted bubble already lives here (preserved by
|
|
3435
|
+
// `data-preserve-runtime`), or we skipped the rebuild because the
|
|
3436
|
+
// fingerprint matched. Either way, leave the live wrapper alone.
|
|
3437
|
+
continue;
|
|
3438
|
+
}
|
|
3439
|
+
wrapper.replaceChildren(bubble);
|
|
3440
|
+
wrapper.setAttribute("data-bubble-fp", fingerprint);
|
|
3441
|
+
lastAskBubbleFingerprint.set(messageId, fingerprint);
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
// Drop fingerprints for messages that are no longer present so a future
|
|
3446
|
+
// re-appearance triggers a fresh plugin render.
|
|
3447
|
+
if (lastAskBubbleFingerprint.size > 0) {
|
|
3448
|
+
for (const id of lastAskBubbleFingerprint.keys()) {
|
|
3449
|
+
if (!activeMessageIds.has(id)) lastAskBubbleFingerprint.delete(id);
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
2842
3452
|
};
|
|
2843
3453
|
|
|
2844
3454
|
// Alias for clarity - the implementation handles flicker prevention via typing indicator logic
|
|
@@ -3165,6 +3775,7 @@ export const createAgentExperience = (
|
|
|
3165
3775
|
onArtifactsState(state) {
|
|
3166
3776
|
lastArtifactsState = state;
|
|
3167
3777
|
syncArtifactPane();
|
|
3778
|
+
persistState();
|
|
3168
3779
|
}
|
|
3169
3780
|
});
|
|
3170
3781
|
|
|
@@ -3217,6 +3828,12 @@ export const createAgentExperience = (
|
|
|
3217
3828
|
if (state.messages?.length) {
|
|
3218
3829
|
session.hydrateMessages(state.messages);
|
|
3219
3830
|
}
|
|
3831
|
+
if (state.artifacts?.length) {
|
|
3832
|
+
session.hydrateArtifacts(
|
|
3833
|
+
state.artifacts,
|
|
3834
|
+
state.selectedArtifactId ?? null
|
|
3835
|
+
);
|
|
3836
|
+
}
|
|
3220
3837
|
})
|
|
3221
3838
|
.catch((error) => {
|
|
3222
3839
|
if (typeof console !== "undefined") {
|
|
@@ -4068,6 +4685,9 @@ export const createAgentExperience = (
|
|
|
4068
4685
|
messageCache.clear();
|
|
4069
4686
|
resumeAutoScroll();
|
|
4070
4687
|
|
|
4688
|
+
// Drop any open ask_user_question sheets — their source messages are gone.
|
|
4689
|
+
removeAskUserQuestionSheet(panelElements.composerOverlay);
|
|
4690
|
+
|
|
4071
4691
|
// Always clear the default localStorage key
|
|
4072
4692
|
try {
|
|
4073
4693
|
localStorage.removeItem(DEFAULT_CHAT_HISTORY_STORAGE_KEY);
|
|
@@ -5743,6 +6363,12 @@ export const createAgentExperience = (
|
|
|
5743
6363
|
if (!artifactsSidebarEnabled(config)) return;
|
|
5744
6364
|
session.clearArtifacts();
|
|
5745
6365
|
},
|
|
6366
|
+
getArtifacts(): PersonaArtifactRecord[] {
|
|
6367
|
+
return session?.getArtifacts() ?? [];
|
|
6368
|
+
},
|
|
6369
|
+
getSelectedArtifactId(): string | null {
|
|
6370
|
+
return session?.getSelectedArtifactId() ?? null;
|
|
6371
|
+
},
|
|
5746
6372
|
focusInput(): boolean {
|
|
5747
6373
|
if (launcherEnabled && !open) return false;
|
|
5748
6374
|
if (!textarea) return false;
|