@artooi/ag-ui-web-component 0.16.0 → 0.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/CHANGELOG.md +132 -1
- package/README.md +40 -5
- package/dist/ag-ui-web-component.bundle.js +109 -31
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +12 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +41 -2
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +361 -67
- package/dist/index.js.map +4 -4
- package/dist/ui/attach_copy_buttons.d.ts +19 -0
- package/dist/ui/attach_copy_buttons.d.ts.map +1 -0
- package/dist/ui/attachment_tray.d.ts +2 -0
- package/dist/ui/attachment_tray.d.ts.map +1 -1
- package/dist/ui/checkpoint_menu.d.ts.map +1 -1
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +8 -0
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +13 -0
- package/src/core/ag_ui_chat.ts +181 -14
- package/src/index.ts +2 -0
- package/src/ui/attach_copy_buttons.ts +82 -0
- package/src/ui/attachment_tray.ts +5 -0
- package/src/ui/checkpoint_menu.ts +59 -6
- package/src/ui/styles.ts +84 -6
- package/src/ui/ui_strings.ts +13 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { UiStrings } from "./ui_strings.js";
|
|
2
|
+
|
|
3
|
+
/** How long the button shows its "copied" confirmation before reverting. */
|
|
4
|
+
const CONFIRM_MS = 1500;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Give every fenced code block in `root` a copy button.
|
|
8
|
+
*
|
|
9
|
+
* An agent that answers with code is answering with something the reader means
|
|
10
|
+
* to *use*, and selecting it by hand out of a scrolling transcript — inside a
|
|
11
|
+
* shadow root, in a narrow sidebar — is the one interaction the chat surface
|
|
12
|
+
* made harder than the page around it.
|
|
13
|
+
*
|
|
14
|
+
* Applied to **finished** bubbles only. The streaming bubble reassigns its
|
|
15
|
+
* `innerHTML` on every delta, so a button attached mid-stream would be
|
|
16
|
+
* discarded and rebuilt for each one; waiting until the turn ends costs
|
|
17
|
+
* nothing and does that work once.
|
|
18
|
+
*
|
|
19
|
+
* Idempotent: a bubble already processed is skipped, so a re-render or a second
|
|
20
|
+
* call cannot stack buttons.
|
|
21
|
+
*/
|
|
22
|
+
export function attachCopyButtons(root: ParentNode, strings: UiStrings): void {
|
|
23
|
+
for (const pre of Array.from(root.querySelectorAll("pre"))) {
|
|
24
|
+
const code = pre.querySelector("code");
|
|
25
|
+
if (code === null || pre.querySelector(".code-copy") !== null) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
pre.classList.add("has-copy");
|
|
29
|
+
pre.append(button(code, strings));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function button(code: Element, strings: UiStrings): HTMLButtonElement {
|
|
34
|
+
// Never null: `textContent` is nullable only on documents and doctypes,
|
|
35
|
+
// and this is always the `code` element `querySelector` just returned.
|
|
36
|
+
const text = code.textContent as string;
|
|
37
|
+
const copy = document.createElement("button");
|
|
38
|
+
copy.type = "button";
|
|
39
|
+
copy.className = "code-copy";
|
|
40
|
+
copy.setAttribute("part", "code-copy");
|
|
41
|
+
copy.textContent = strings.copyCode;
|
|
42
|
+
copy.title = strings.copyCode;
|
|
43
|
+
copy.setAttribute("aria-label", strings.copyCode);
|
|
44
|
+
copy.addEventListener("click", () => {
|
|
45
|
+
void writeText(text).then((ok) => {
|
|
46
|
+
// Say which of the two happened. A button that always claims success is
|
|
47
|
+
// worse than no button: the reader pastes stale clipboard content and
|
|
48
|
+
// finds out somewhere else entirely.
|
|
49
|
+
confirm(copy, ok ? strings.copied : strings.copyFailed, strings);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
return copy;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Copy `text`, reporting whether it landed.
|
|
57
|
+
*
|
|
58
|
+
* The Clipboard API needs a secure context and a user gesture, and is simply
|
|
59
|
+
* absent in some embeddings, so its absence is an ordinary outcome rather than
|
|
60
|
+
* an error worth throwing at the host page.
|
|
61
|
+
*/
|
|
62
|
+
async function writeText(text: string): Promise<boolean> {
|
|
63
|
+
const clipboard = navigator.clipboard;
|
|
64
|
+
if (clipboard === undefined) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
await clipboard.writeText(text);
|
|
69
|
+
return true;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function confirm(copy: HTMLButtonElement, message: string, strings: UiStrings): void {
|
|
76
|
+
copy.textContent = message;
|
|
77
|
+
copy.dataset["state"] = message === strings.copied ? "copied" : "failed";
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
copy.textContent = strings.copyCode;
|
|
80
|
+
delete copy.dataset["state"];
|
|
81
|
+
}, CONFIRM_MS);
|
|
82
|
+
}
|
|
@@ -102,6 +102,11 @@ export class AttachmentTray {
|
|
|
102
102
|
return this.#items.some((item) => item.status === ATTACHMENT_STATUS.UPLOADING);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/** How many chips are still uploading — the count a send-time notice names. */
|
|
106
|
+
pendingCount(): number {
|
|
107
|
+
return this.#items.filter((item) => item.status === ATTACHMENT_STATUS.UPLOADING).length;
|
|
108
|
+
}
|
|
109
|
+
|
|
105
110
|
/** Whether the tray holds no chips. */
|
|
106
111
|
isEmpty(): boolean {
|
|
107
112
|
return this.#items.length === 0;
|
|
@@ -26,6 +26,8 @@ export class CheckpointMenu {
|
|
|
26
26
|
readonly #onPick: (runId: string, verb: CheckpointVerb) => void;
|
|
27
27
|
readonly #list: HTMLDivElement;
|
|
28
28
|
readonly #heading: HTMLSpanElement;
|
|
29
|
+
/** What had focus before the panel opened, restored on close. */
|
|
30
|
+
#lastFocused: HTMLElement | null = null;
|
|
29
31
|
#strings: UiStrings;
|
|
30
32
|
#runs: readonly RunRow[] = [];
|
|
31
33
|
|
|
@@ -41,6 +43,10 @@ export class CheckpointMenu {
|
|
|
41
43
|
this.element.setAttribute("part", "checkpoints");
|
|
42
44
|
this.element.setAttribute("role", "dialog");
|
|
43
45
|
this.element.setAttribute("aria-label", strings.checkpoints);
|
|
46
|
+
// Focusable as a fallback: with no continuable runs the panel holds no
|
|
47
|
+
// controls at all, and a dialog that cannot take focus strands the
|
|
48
|
+
// keyboard user outside it with no way back except Tab-ing blind.
|
|
49
|
+
this.element.tabIndex = -1;
|
|
44
50
|
this.element.hidden = true;
|
|
45
51
|
|
|
46
52
|
const header = document.createElement("div");
|
|
@@ -48,6 +54,7 @@ export class CheckpointMenu {
|
|
|
48
54
|
header.setAttribute("part", "checkpoints-header");
|
|
49
55
|
this.#heading = document.createElement("span");
|
|
50
56
|
this.#heading.className = "checkpoints-title";
|
|
57
|
+
this.#heading.setAttribute("part", "checkpoints-title");
|
|
51
58
|
this.#heading.textContent = strings.checkpoints;
|
|
52
59
|
header.append(this.#heading);
|
|
53
60
|
|
|
@@ -56,12 +63,7 @@ export class CheckpointMenu {
|
|
|
56
63
|
this.#list.setAttribute("part", "checkpoints-list");
|
|
57
64
|
|
|
58
65
|
this.element.append(header, this.#list);
|
|
59
|
-
this.element.addEventListener("keydown", (event) =>
|
|
60
|
-
if (event.key === "Escape") {
|
|
61
|
-
event.stopPropagation();
|
|
62
|
-
this.close();
|
|
63
|
-
}
|
|
64
|
-
});
|
|
66
|
+
this.element.addEventListener("keydown", (event) => this.#onKeydown(event));
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
/** Replace the rows. The host passes only `continuable` runs. */
|
|
@@ -79,11 +81,61 @@ export class CheckpointMenu {
|
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
open(): void {
|
|
84
|
+
if (this.open_) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// Remember what had focus so it is restored on close, then move focus into
|
|
88
|
+
// the panel so a keyboard user lands inside the dialog rather than being
|
|
89
|
+
// left behind it. The thread drawer already does this; a dialog that takes
|
|
90
|
+
// no focus is one a keyboard user cannot reach and a screen reader does not
|
|
91
|
+
// announce, however correct its role and label.
|
|
92
|
+
this.#lastFocused = this.#activeElement() as HTMLElement | null;
|
|
82
93
|
this.element.hidden = false;
|
|
94
|
+
(this.#focusables()[0] ?? this.element).focus();
|
|
83
95
|
}
|
|
84
96
|
|
|
85
97
|
close(): void {
|
|
98
|
+
if (!this.open_) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
86
101
|
this.element.hidden = true;
|
|
102
|
+
this.#lastFocused?.focus();
|
|
103
|
+
this.#lastFocused = null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The focused element within this panel's root (shadow-aware). */
|
|
107
|
+
#activeElement(): Element | null {
|
|
108
|
+
return (this.element.getRootNode() as Document | ShadowRoot).activeElement;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The panel's tabbable controls, in document order. */
|
|
112
|
+
#focusables(): HTMLElement[] {
|
|
113
|
+
return Array.from(this.element.querySelectorAll<HTMLElement>("button, [tabindex]")).filter(
|
|
114
|
+
(el) => !el.hidden,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Escape closes; Tab is trapped so focus cannot wander behind the dialog. */
|
|
119
|
+
#onKeydown(event: KeyboardEvent): void {
|
|
120
|
+
if (event.key === "Escape") {
|
|
121
|
+
event.stopPropagation();
|
|
122
|
+
this.close();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (event.key !== "Tab") {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const focusables = this.#focusables();
|
|
129
|
+
const first = focusables[0];
|
|
130
|
+
const last = focusables[focusables.length - 1];
|
|
131
|
+
const active = this.#activeElement();
|
|
132
|
+
if (event.shiftKey && active === first) {
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
last?.focus();
|
|
135
|
+
} else if (!event.shiftKey && active === last) {
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
first?.focus();
|
|
138
|
+
}
|
|
87
139
|
}
|
|
88
140
|
|
|
89
141
|
get open_(): boolean {
|
|
@@ -112,6 +164,7 @@ export class CheckpointMenu {
|
|
|
112
164
|
|
|
113
165
|
const label = document.createElement("span");
|
|
114
166
|
label.className = "checkpoint-label";
|
|
167
|
+
label.setAttribute("part", "checkpoint-label");
|
|
115
168
|
// A run id is opaque to a person, so the time is the identifying detail;
|
|
116
169
|
// the id rides `title` for anyone who needs to correlate with server logs.
|
|
117
170
|
label.textContent =
|
package/src/ui/styles.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const STYLES = `
|
|
|
11
11
|
--ag-ui-user-bg: #4f46e5;
|
|
12
12
|
--ag-ui-user-fg: #ffffff;
|
|
13
13
|
--ag-ui-assistant-bg: #f1f1f6;
|
|
14
|
+
--ag-ui-hover: #e7e7ee;
|
|
14
15
|
--ag-ui-input-bg: var(--ag-ui-bg);
|
|
15
16
|
--ag-ui-tool-bg: var(--ag-ui-assistant-bg);
|
|
16
17
|
--ag-ui-tool-fg: var(--ag-ui-accent);
|
|
@@ -80,6 +81,7 @@ export const STYLES = `
|
|
|
80
81
|
--ag-ui-bg: #15151f;
|
|
81
82
|
--ag-ui-fg: #e8e8f2;
|
|
82
83
|
--ag-ui-assistant-bg: #25253a;
|
|
84
|
+
--ag-ui-hover: #303049;
|
|
83
85
|
--ag-ui-header-bg: #1f1f30;
|
|
84
86
|
--ag-ui-header-fg: #e8e8f2;
|
|
85
87
|
--ag-ui-border: #33334a;
|
|
@@ -92,6 +94,7 @@ export const STYLES = `
|
|
|
92
94
|
--ag-ui-bg: #15151f;
|
|
93
95
|
--ag-ui-fg: #e8e8f2;
|
|
94
96
|
--ag-ui-assistant-bg: #25253a;
|
|
97
|
+
--ag-ui-hover: #303049;
|
|
95
98
|
--ag-ui-header-bg: #1f1f30;
|
|
96
99
|
--ag-ui-header-fg: #e8e8f2;
|
|
97
100
|
--ag-ui-border: #33334a;
|
|
@@ -107,6 +110,7 @@ export const STYLES = `
|
|
|
107
110
|
--ag-ui-accent: #3fb950;
|
|
108
111
|
--ag-ui-user-bg: #238636;
|
|
109
112
|
--ag-ui-assistant-bg: #161b22;
|
|
113
|
+
--ag-ui-hover: #21262d;
|
|
110
114
|
--ag-ui-header-bg: #010409;
|
|
111
115
|
--ag-ui-header-fg: #c9d1d9;
|
|
112
116
|
--ag-ui-border: #30363d;
|
|
@@ -508,12 +512,86 @@ export const STYLES = `
|
|
|
508
512
|
padding: 0;
|
|
509
513
|
}
|
|
510
514
|
|
|
515
|
+
/* The copy button sits inside the block, so it scrolls with wide code rather
|
|
516
|
+
than floating over the bubble. Positioning is on the pre; the button only
|
|
517
|
+
appears once one has been attached. */
|
|
518
|
+
.message--assistant pre.has-copy {
|
|
519
|
+
position: relative;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.code-copy {
|
|
523
|
+
position: absolute;
|
|
524
|
+
top: 4px;
|
|
525
|
+
right: 4px;
|
|
526
|
+
padding: 2px 8px;
|
|
527
|
+
font: inherit;
|
|
528
|
+
font-size: 0.75em;
|
|
529
|
+
line-height: 1.6;
|
|
530
|
+
color: var(--ag-ui-muted);
|
|
531
|
+
background: var(--ag-ui-surface);
|
|
532
|
+
border: 1px solid var(--ag-ui-border);
|
|
533
|
+
border-radius: 4px;
|
|
534
|
+
cursor: pointer;
|
|
535
|
+
opacity: 0;
|
|
536
|
+
transition: opacity 0.12s ease;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/* Revealed on hover or keyboard focus. Focus matters as much as hover here:
|
|
540
|
+
hidden-until-hover is invisible to a keyboard user otherwise. */
|
|
541
|
+
.message--assistant pre.has-copy:hover .code-copy,
|
|
542
|
+
.code-copy:focus-visible {
|
|
543
|
+
opacity: 1;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.code-copy:hover {
|
|
547
|
+
color: var(--ag-ui-text);
|
|
548
|
+
background: var(--ag-ui-hover);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.code-copy[data-state="copied"] {
|
|
552
|
+
opacity: 1;
|
|
553
|
+
color: var(--ag-ui-text);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.code-copy[data-state="failed"] {
|
|
557
|
+
opacity: 1;
|
|
558
|
+
color: var(--ag-ui-danger, var(--ag-ui-text));
|
|
559
|
+
}
|
|
560
|
+
|
|
511
561
|
.message--assistant blockquote {
|
|
512
562
|
padding-left: 10px;
|
|
513
563
|
border-left: 3px solid var(--ag-ui-border);
|
|
514
564
|
color: var(--ag-ui-muted);
|
|
515
565
|
}
|
|
516
566
|
|
|
567
|
+
/* Markdown tables. table/thead/tbody/tr/th/td are all in
|
|
568
|
+
the sanitizer's ALLOWED_TAGS, so an agent emitting one renders it — and until
|
|
569
|
+
now rendered it entirely unstyled, overflowing the bubble. A wide table
|
|
570
|
+
scrolls inside its own box rather than stretching the message: the bubble is
|
|
571
|
+
width-constrained, so without this the columns either crush or push the
|
|
572
|
+
layout sideways. */
|
|
573
|
+
.message--assistant table {
|
|
574
|
+
display: block;
|
|
575
|
+
width: fit-content;
|
|
576
|
+
max-width: 100%;
|
|
577
|
+
overflow-x: auto;
|
|
578
|
+
border-collapse: collapse;
|
|
579
|
+
font-size: 0.95em;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.message--assistant th,
|
|
583
|
+
.message--assistant td {
|
|
584
|
+
padding: 6px 10px;
|
|
585
|
+
border: 1px solid var(--ag-ui-border);
|
|
586
|
+
text-align: left;
|
|
587
|
+
vertical-align: top;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.message--assistant th {
|
|
591
|
+
background: var(--ag-ui-hover);
|
|
592
|
+
font-weight: 600;
|
|
593
|
+
}
|
|
594
|
+
|
|
517
595
|
.pending {
|
|
518
596
|
align-self: flex-start;
|
|
519
597
|
display: inline-flex;
|
|
@@ -1264,9 +1342,9 @@ export const STYLES = `
|
|
|
1264
1342
|
flex-direction: column;
|
|
1265
1343
|
gap: 0.25rem;
|
|
1266
1344
|
padding: 0.5rem;
|
|
1267
|
-
border: 1px solid var(--
|
|
1345
|
+
border: 1px solid var(--ag-ui-border);
|
|
1268
1346
|
border-radius: 0.5rem;
|
|
1269
|
-
background: var(--
|
|
1347
|
+
background: var(--ag-ui-assistant-bg);
|
|
1270
1348
|
box-shadow: 0 6px 24px rgb(0 0 0 / 12%);
|
|
1271
1349
|
max-height: 60%;
|
|
1272
1350
|
overflow-y: auto;
|
|
@@ -1297,7 +1375,7 @@ export const STYLES = `
|
|
|
1297
1375
|
}
|
|
1298
1376
|
|
|
1299
1377
|
.checkpoint-row:hover {
|
|
1300
|
-
background: var(--
|
|
1378
|
+
background: var(--ag-ui-hover);
|
|
1301
1379
|
}
|
|
1302
1380
|
|
|
1303
1381
|
.checkpoint-label {
|
|
@@ -1312,7 +1390,7 @@ export const STYLES = `
|
|
|
1312
1390
|
font-size: 0.6875rem;
|
|
1313
1391
|
padding: 0 0.375rem;
|
|
1314
1392
|
border-radius: 999px;
|
|
1315
|
-
background: var(--
|
|
1393
|
+
background: var(--ag-ui-hover);
|
|
1316
1394
|
opacity: 0.8;
|
|
1317
1395
|
}
|
|
1318
1396
|
|
|
@@ -1321,14 +1399,14 @@ export const STYLES = `
|
|
|
1321
1399
|
font-size: 0.75rem;
|
|
1322
1400
|
cursor: pointer;
|
|
1323
1401
|
padding: 0.125rem 0.5rem;
|
|
1324
|
-
border: 1px solid var(--
|
|
1402
|
+
border: 1px solid var(--ag-ui-border);
|
|
1325
1403
|
border-radius: 0.375rem;
|
|
1326
1404
|
background: transparent;
|
|
1327
1405
|
color: inherit;
|
|
1328
1406
|
}
|
|
1329
1407
|
|
|
1330
1408
|
.checkpoint-action:hover {
|
|
1331
|
-
background: var(--
|
|
1409
|
+
background: var(--ag-ui-hover);
|
|
1332
1410
|
}
|
|
1333
1411
|
|
|
1334
1412
|
.drawer-backdrop {
|
package/src/ui/ui_strings.ts
CHANGED
|
@@ -57,6 +57,8 @@ export interface UiStrings {
|
|
|
57
57
|
runInterrupted: string;
|
|
58
58
|
/** Tool-result content (and card text) when the page moved mid-run. */
|
|
59
59
|
pageMoved: string;
|
|
60
|
+
/** Notice when Send ran while a file was still uploading. Token: `{n}`. */
|
|
61
|
+
attachmentsStillUploading: string;
|
|
60
62
|
|
|
61
63
|
// ── Composer ────────────────────────────────────────────────────────────────
|
|
62
64
|
/** `aria-label` of the message textarea. */
|
|
@@ -166,6 +168,12 @@ export interface UiStrings {
|
|
|
166
168
|
/** Hours ago. Token: `{n}`. */
|
|
167
169
|
hoursAgo: string;
|
|
168
170
|
/** Title of the checkpoint panel. */
|
|
171
|
+
/** Label on a code block's copy button. */
|
|
172
|
+
copyCode: string;
|
|
173
|
+
/** Shown on the copy button after the code reached the clipboard. */
|
|
174
|
+
copied: string;
|
|
175
|
+
/** Shown when the clipboard was unavailable or refused the write. */
|
|
176
|
+
copyFailed: string;
|
|
169
177
|
checkpoints: string;
|
|
170
178
|
/** Empty state when no run can be continued. */
|
|
171
179
|
noCheckpoints: string;
|
|
@@ -189,6 +197,9 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
|
|
|
189
197
|
collapse: "Collapse",
|
|
190
198
|
expand: "Expand",
|
|
191
199
|
toggleTheme: "Toggle theme",
|
|
200
|
+
copyCode: "Copy",
|
|
201
|
+
copied: "Copied",
|
|
202
|
+
copyFailed: "Copy failed",
|
|
192
203
|
checkpoints: "Continue a run",
|
|
193
204
|
noCheckpoints: "Nothing to continue yet.",
|
|
194
205
|
resumeRun: "Resume",
|
|
@@ -208,6 +219,8 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
|
|
|
208
219
|
runInterrupted: "The previous response didn’t finish — the page changed before it arrived.",
|
|
209
220
|
pageMoved:
|
|
210
221
|
"The page changed since you last looked at it. Call read_page to see the current page, then retry.",
|
|
222
|
+
attachmentsStillUploading:
|
|
223
|
+
"{n} file still uploading — it was not sent with this message and is still attached.",
|
|
211
224
|
skillNeeds: "“{title}” needs: {fields}",
|
|
212
225
|
|
|
213
226
|
message: "Message",
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION: string = "0.
|
|
1
|
+
export const VERSION: string = "0.18.0";
|