@artooi/ag-ui-web-component 0.11.0 → 0.12.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.
@@ -0,0 +1,153 @@
1
+ import type { RunRow } from "../core/run_index.js";
2
+ import { relativeTime } from "./relative_time.js";
3
+ import { DEFAULT_UI_STRINGS, type UiStrings } from "./ui_strings.js";
4
+
5
+ /** How the host continues a picked run. */
6
+ export type CheckpointVerb = "resume" | "fork";
7
+
8
+ /**
9
+ * The checkpoint panel: continuable runs, each offering **resume** or **fork**.
10
+ *
11
+ * A separate surface from the thread drawer on purpose — they are different
12
+ * axes. A thread is a conversation you switch *to*; a checkpoint is a run you
13
+ * continue *from*, and one thread can hold many. Folding them into one list
14
+ * would make "resume" look like "open", which it isn't: resuming starts a new
15
+ * run seeded from a snapshot.
16
+ *
17
+ * Only rows the server marked `continuable` are worth offering, so the host
18
+ * feeds those; a run with no snapshot would resume from nothing. Pure DOM in
19
+ * the spirit of {@link SkillsMenu} — the host appends {@link element}, toggles
20
+ * it, feeds rows via {@link setRuns}, and acts on {@link onPick}.
21
+ */
22
+ export class CheckpointMenu {
23
+ /** The panel root. Append to the chat shell; hidden until opened. */
24
+ readonly element: HTMLDivElement;
25
+
26
+ readonly #onPick: (runId: string, verb: CheckpointVerb) => void;
27
+ readonly #list: HTMLDivElement;
28
+ readonly #heading: HTMLSpanElement;
29
+ #strings: UiStrings;
30
+ #runs: readonly RunRow[] = [];
31
+
32
+ constructor(
33
+ onPick: (runId: string, verb: CheckpointVerb) => void,
34
+ strings: UiStrings = DEFAULT_UI_STRINGS,
35
+ ) {
36
+ this.#onPick = onPick;
37
+ this.#strings = strings;
38
+
39
+ this.element = document.createElement("div");
40
+ this.element.className = "checkpoints";
41
+ this.element.setAttribute("part", "checkpoints");
42
+ this.element.setAttribute("role", "dialog");
43
+ this.element.setAttribute("aria-label", strings.checkpoints);
44
+ this.element.hidden = true;
45
+
46
+ const header = document.createElement("div");
47
+ header.className = "checkpoints-header";
48
+ header.setAttribute("part", "checkpoints-header");
49
+ this.#heading = document.createElement("span");
50
+ this.#heading.className = "checkpoints-title";
51
+ this.#heading.textContent = strings.checkpoints;
52
+ header.append(this.#heading);
53
+
54
+ this.#list = document.createElement("div");
55
+ this.#list.className = "checkpoints-list";
56
+ this.#list.setAttribute("part", "checkpoints-list");
57
+
58
+ 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
+ });
65
+ }
66
+
67
+ /** Replace the rows. The host passes only `continuable` runs. */
68
+ setRuns(runs: readonly RunRow[]): void {
69
+ this.#runs = runs;
70
+ this.#render();
71
+ }
72
+
73
+ /** Re-localize a panel built before the host's strings resolved. */
74
+ setStrings(strings: UiStrings): void {
75
+ this.#strings = strings;
76
+ this.element.setAttribute("aria-label", strings.checkpoints);
77
+ this.#heading.textContent = strings.checkpoints;
78
+ this.#render();
79
+ }
80
+
81
+ open(): void {
82
+ this.element.hidden = false;
83
+ }
84
+
85
+ close(): void {
86
+ this.element.hidden = true;
87
+ }
88
+
89
+ get open_(): boolean {
90
+ return !this.element.hidden;
91
+ }
92
+
93
+ #render(): void {
94
+ this.#list.replaceChildren();
95
+ if (this.#runs.length === 0) {
96
+ const empty = document.createElement("div");
97
+ empty.className = "checkpoints-empty";
98
+ empty.setAttribute("part", "checkpoints-empty");
99
+ empty.textContent = this.#strings.noCheckpoints;
100
+ this.#list.append(empty);
101
+ return;
102
+ }
103
+ for (const run of this.#runs) {
104
+ this.#list.append(this.#row(run));
105
+ }
106
+ }
107
+
108
+ #row(run: RunRow): HTMLDivElement {
109
+ const row = document.createElement("div");
110
+ row.className = "checkpoint-row";
111
+ row.setAttribute("part", "checkpoint-row");
112
+
113
+ const label = document.createElement("span");
114
+ label.className = "checkpoint-label";
115
+ // A run id is opaque to a person, so the time is the identifying detail;
116
+ // the id rides `title` for anyone who needs to correlate with server logs.
117
+ label.textContent =
118
+ run.started_at === null
119
+ ? run.run_id
120
+ : relativeTime(Date.parse(run.started_at), Date.now(), this.#strings);
121
+ label.title = run.run_id;
122
+ row.append(label);
123
+
124
+ if (run.parent_run_id !== null) {
125
+ // Lineage, so a branch doesn't read as a duplicate of its parent.
126
+ const branch = document.createElement("span");
127
+ branch.className = "checkpoint-branch";
128
+ branch.setAttribute("part", "checkpoint-branch");
129
+ branch.textContent = this.#strings.forkedRun;
130
+ branch.title = run.parent_run_id;
131
+ row.append(branch);
132
+ }
133
+
134
+ row.append(
135
+ this.#action(run.run_id, "resume", this.#strings.resumeRun),
136
+ this.#action(run.run_id, "fork", this.#strings.forkRun),
137
+ );
138
+ return row;
139
+ }
140
+
141
+ #action(runId: string, verb: CheckpointVerb, label: string): HTMLButtonElement {
142
+ const button = document.createElement("button");
143
+ button.type = "button";
144
+ button.className = `checkpoint-action checkpoint-${verb}`;
145
+ button.setAttribute("part", `checkpoint-action checkpoint-${verb}`);
146
+ button.textContent = label;
147
+ button.addEventListener("click", () => {
148
+ this.close();
149
+ this.#onPick(runId, verb);
150
+ });
151
+ return button;
152
+ }
153
+ }
package/src/ui/styles.ts CHANGED
@@ -1229,6 +1229,82 @@ export const STYLES = `
1229
1229
  display: none;
1230
1230
  }
1231
1231
 
1232
+ .checkpoints {
1233
+ position: absolute;
1234
+ inset-block-start: 3rem;
1235
+ inset-inline: 0.75rem;
1236
+ z-index: 6;
1237
+ display: flex;
1238
+ flex-direction: column;
1239
+ gap: 0.25rem;
1240
+ padding: 0.5rem;
1241
+ border: 1px solid var(--agui-border, #d7d7dc);
1242
+ border-radius: 0.5rem;
1243
+ background: var(--agui-surface, #fff);
1244
+ box-shadow: 0 6px 24px rgb(0 0 0 / 12%);
1245
+ max-height: 60%;
1246
+ overflow-y: auto;
1247
+ }
1248
+
1249
+ .checkpoints[hidden] {
1250
+ display: none;
1251
+ }
1252
+
1253
+ .checkpoints-title {
1254
+ font-size: 0.75rem;
1255
+ font-weight: 600;
1256
+ opacity: 0.7;
1257
+ }
1258
+
1259
+ .checkpoints-empty {
1260
+ padding: 0.5rem 0.25rem;
1261
+ font-size: 0.8125rem;
1262
+ opacity: 0.7;
1263
+ }
1264
+
1265
+ .checkpoint-row {
1266
+ display: flex;
1267
+ align-items: center;
1268
+ gap: 0.5rem;
1269
+ padding: 0.25rem;
1270
+ border-radius: 0.375rem;
1271
+ }
1272
+
1273
+ .checkpoint-row:hover {
1274
+ background: var(--agui-hover, #f3f3f5);
1275
+ }
1276
+
1277
+ .checkpoint-label {
1278
+ flex: 1;
1279
+ font-size: 0.8125rem;
1280
+ white-space: nowrap;
1281
+ overflow: hidden;
1282
+ text-overflow: ellipsis;
1283
+ }
1284
+
1285
+ .checkpoint-branch {
1286
+ font-size: 0.6875rem;
1287
+ padding: 0 0.375rem;
1288
+ border-radius: 999px;
1289
+ background: var(--agui-hover, #f3f3f5);
1290
+ opacity: 0.8;
1291
+ }
1292
+
1293
+ .checkpoint-action {
1294
+ font: inherit;
1295
+ font-size: 0.75rem;
1296
+ cursor: pointer;
1297
+ padding: 0.125rem 0.5rem;
1298
+ border: 1px solid var(--agui-border, #d7d7dc);
1299
+ border-radius: 0.375rem;
1300
+ background: transparent;
1301
+ color: inherit;
1302
+ }
1303
+
1304
+ .checkpoint-action:hover {
1305
+ background: var(--agui-hover, #f3f3f5);
1306
+ }
1307
+
1232
1308
  .drawer-backdrop {
1233
1309
  position: absolute;
1234
1310
  inset: 0;
@@ -157,6 +157,16 @@ export interface UiStrings {
157
157
  minutesAgo: string;
158
158
  /** Hours ago. Token: `{n}`. */
159
159
  hoursAgo: string;
160
+ /** Title of the checkpoint panel. */
161
+ checkpoints: string;
162
+ /** Empty state when no run can be continued. */
163
+ noCheckpoints: string;
164
+ /** Action: continue a run from its last checkpoint. */
165
+ resumeRun: string;
166
+ /** Action: branch a run without touching the original. */
167
+ forkRun: string;
168
+ /** Badge on a run that branched from another. */
169
+ forkedRun: string;
160
170
  /** Days ago. Token: `{n}`. */
161
171
  daysAgo: string;
162
172
  /** Weeks ago. Token: `{n}`. */
@@ -171,6 +181,11 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
171
181
  collapse: "Collapse",
172
182
  expand: "Expand",
173
183
  toggleTheme: "Toggle theme",
184
+ checkpoints: "Continue a run",
185
+ noCheckpoints: "Nothing to continue yet.",
186
+ resumeRun: "Resume",
187
+ forkRun: "Fork",
188
+ forkedRun: "branched",
174
189
 
175
190
  conversation: "Conversation",
176
191
  thinking: "Assistant is thinking…",
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION: string = "0.11.0";
1
+ export const VERSION: string = "0.12.0";