@frockbot/plugin-shell 0.3.22 → 0.3.23
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/package.json +32 -32
- package/src/backend-recovery-integration.test.ts +134 -0
- package/src/backend.ts +93 -42
- package/src/client/AppletCanvas.vue +128 -30
- package/src/client/FrockBotApp.vue +111 -2
- package/src/client/applet-building-view.test.ts +69 -0
- package/src/client/applet-progress.test.ts +300 -0
- package/src/client/applet-progress.ts +339 -0
- package/src/client/deployment.test.ts +152 -0
- package/src/client/deployment.ts +138 -0
- package/src/client/index.test.ts +71 -0
- package/src/client/index.ts +58 -0
- package/src/client/styles.css +91 -4
- package/src/run-protocol.ts +10 -0
- package/src/shared.ts +14 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-shell",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.23",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -52,42 +52,42 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@cordisjs/plugin-webui": "0.8.2",
|
|
55
|
-
"@frockbot/agent-runtime": "0.3.
|
|
56
|
-
"@frockbot/application-foundation": "0.3.
|
|
57
|
-
"@frockbot/catalog-core": "0.3.
|
|
58
|
-
"@frockbot/client-core": "0.3.
|
|
59
|
-
"@frockbot/client-ui": "0.3.
|
|
60
|
-
"@frockbot/computer-core": "0.3.
|
|
61
|
-
"@frockbot/computer-host-protocol": "0.3.
|
|
62
|
-
"@frockbot/configuration-core": "0.3.
|
|
63
|
-
"@frockbot/connection-core": "0.3.
|
|
64
|
-
"@frockbot/kernel-agent-loop": "0.3.
|
|
65
|
-
"@frockbot/kernel-composition": "0.3.
|
|
66
|
-
"@frockbot/kernel-contracts": "0.3.
|
|
67
|
-
"@frockbot/kernel-do": "0.3.
|
|
68
|
-
"@frockbot/machine-protocol": "0.3.
|
|
69
|
-
"@frockbot/plugin-applets": "0.3.
|
|
70
|
-
"@frockbot/plugin-authoring": "0.3.
|
|
71
|
-
"@frockbot/plugin-bot-template": "0.3.
|
|
72
|
-
"@frockbot/plugin-computer": "0.3.
|
|
73
|
-
"@frockbot/plugin-flock": "0.3.
|
|
74
|
-
"@frockbot/plugin-image": "0.3.
|
|
75
|
-
"@frockbot/plugin-machine-messages": "0.3.
|
|
76
|
-
"@frockbot/plugin-mcp": "0.3.
|
|
77
|
-
"@frockbot/plugin-memory": "0.3.
|
|
78
|
-
"@frockbot/plugin-package-catalog": "0.3.
|
|
79
|
-
"@frockbot/plugin-routines": "0.3.
|
|
80
|
-
"@frockbot/plugin-skills": "0.3.
|
|
81
|
-
"@frockbot/plugin-subagents": "0.3.
|
|
82
|
-
"@frockbot/plugin-user-machine": "0.3.
|
|
83
|
-
"@frockbot/protocol": "0.3.
|
|
55
|
+
"@frockbot/agent-runtime": "0.3.23",
|
|
56
|
+
"@frockbot/application-foundation": "0.3.23",
|
|
57
|
+
"@frockbot/catalog-core": "0.3.23",
|
|
58
|
+
"@frockbot/client-core": "0.3.23",
|
|
59
|
+
"@frockbot/client-ui": "0.3.23",
|
|
60
|
+
"@frockbot/computer-core": "0.3.23",
|
|
61
|
+
"@frockbot/computer-host-protocol": "0.3.23",
|
|
62
|
+
"@frockbot/configuration-core": "0.3.23",
|
|
63
|
+
"@frockbot/connection-core": "0.3.23",
|
|
64
|
+
"@frockbot/kernel-agent-loop": "0.3.23",
|
|
65
|
+
"@frockbot/kernel-composition": "0.3.23",
|
|
66
|
+
"@frockbot/kernel-contracts": "0.3.23",
|
|
67
|
+
"@frockbot/kernel-do": "0.3.23",
|
|
68
|
+
"@frockbot/machine-protocol": "0.3.23",
|
|
69
|
+
"@frockbot/plugin-applets": "0.3.23",
|
|
70
|
+
"@frockbot/plugin-authoring": "0.3.23",
|
|
71
|
+
"@frockbot/plugin-bot-template": "0.3.23",
|
|
72
|
+
"@frockbot/plugin-computer": "0.3.23",
|
|
73
|
+
"@frockbot/plugin-flock": "0.3.23",
|
|
74
|
+
"@frockbot/plugin-image": "0.3.23",
|
|
75
|
+
"@frockbot/plugin-machine-messages": "0.3.23",
|
|
76
|
+
"@frockbot/plugin-mcp": "0.3.23",
|
|
77
|
+
"@frockbot/plugin-memory": "0.3.23",
|
|
78
|
+
"@frockbot/plugin-package-catalog": "0.3.23",
|
|
79
|
+
"@frockbot/plugin-routines": "0.3.23",
|
|
80
|
+
"@frockbot/plugin-skills": "0.3.23",
|
|
81
|
+
"@frockbot/plugin-subagents": "0.3.23",
|
|
82
|
+
"@frockbot/plugin-user-machine": "0.3.23",
|
|
83
|
+
"@frockbot/protocol": "0.3.23",
|
|
84
84
|
"cordis": "4.0.0-rc.8",
|
|
85
85
|
"vue": "3.5.41"
|
|
86
86
|
},
|
|
87
87
|
"devDependencies": {
|
|
88
88
|
"@cloudflare/workers-types": "latest",
|
|
89
|
-
"@frockbot/plugin-prompt": "0.3.
|
|
90
|
-
"@frockbot/plugin-tools": "0.3.
|
|
89
|
+
"@frockbot/plugin-prompt": "0.3.23",
|
|
90
|
+
"@frockbot/plugin-tools": "0.3.23",
|
|
91
91
|
"@types/bun": "1.3.6",
|
|
92
92
|
"@types/node": "26.2.0",
|
|
93
93
|
"@vitejs/plugin-vue": "6.0.8",
|
|
@@ -19,6 +19,7 @@ import { planBotRunRecovery } from "./backend-recovery.js";
|
|
|
19
19
|
import {
|
|
20
20
|
CLIENT_RUN_LIST_MAX_BYTES,
|
|
21
21
|
CLIENT_RUN_PAGE_LIMIT,
|
|
22
|
+
CLIENT_RUN_SCAN_LIMIT,
|
|
22
23
|
clientRunListWireBytes,
|
|
23
24
|
} from "./run-protocol.js";
|
|
24
25
|
|
|
@@ -1475,6 +1476,139 @@ describe("Bot recovery", () => {
|
|
|
1475
1476
|
).toBe(false);
|
|
1476
1477
|
});
|
|
1477
1478
|
|
|
1479
|
+
/**
|
|
1480
|
+
* The run index is global; the transcript is one conversation. These cover
|
|
1481
|
+
* the shape that used to answer "this conversation is empty" whenever the
|
|
1482
|
+
* newest page of the index happened to hold nothing the reader could see.
|
|
1483
|
+
*/
|
|
1484
|
+
async function writeRunHistory(
|
|
1485
|
+
storage: MemoryStorage,
|
|
1486
|
+
runs: Array<{
|
|
1487
|
+
runId: string;
|
|
1488
|
+
sessionId: string;
|
|
1489
|
+
turnType?: "chat" | "automation";
|
|
1490
|
+
}>,
|
|
1491
|
+
): Promise<void> {
|
|
1492
|
+
const baseTime = Date.parse("2026-09-01T00:00:00.000Z");
|
|
1493
|
+
for (const [index, entry] of runs.entries()) {
|
|
1494
|
+
const acceptedAt = new Date(baseTime + index * 1_000).toISOString();
|
|
1495
|
+
const run = {
|
|
1496
|
+
runId: entry.runId,
|
|
1497
|
+
commandFingerprint: `fingerprint-${index}`,
|
|
1498
|
+
sessionId: entry.sessionId,
|
|
1499
|
+
acceptedAt,
|
|
1500
|
+
input: "hello",
|
|
1501
|
+
events: [],
|
|
1502
|
+
effectAdmissions: [],
|
|
1503
|
+
status: "completed",
|
|
1504
|
+
phase: "executing",
|
|
1505
|
+
compositionGenerationId: "test-composition-generation",
|
|
1506
|
+
configurationSnapshot: initializeBotSettingsV1("primary"),
|
|
1507
|
+
previousEventCount: 0,
|
|
1508
|
+
responseText: "answer",
|
|
1509
|
+
...(entry.turnType && entry.turnType !== "chat"
|
|
1510
|
+
? { admission: { schemaVersion: 1, turnType: entry.turnType } }
|
|
1511
|
+
: {}),
|
|
1512
|
+
} satisfies StoredRun;
|
|
1513
|
+
await storage.put({
|
|
1514
|
+
[`run:${entry.runId}`]: run,
|
|
1515
|
+
[`run-index:${acceptedAt}:${entry.runId}`]: entry.runId,
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
test("reaches an older conversation buried under newer Turns", async () => {
|
|
1521
|
+
const storage = new MemoryStorage();
|
|
1522
|
+
await writeRunHistory(storage, [
|
|
1523
|
+
{ runId: "run-older", sessionId: "user:primary:older" },
|
|
1524
|
+
...Array.from({ length: CLIENT_RUN_PAGE_LIMIT + 2 }, (_value, index) => ({
|
|
1525
|
+
runId: `run-newer-${index.toString().padStart(3, "0")}`,
|
|
1526
|
+
sessionId: "user:primary:newer",
|
|
1527
|
+
})),
|
|
1528
|
+
]);
|
|
1529
|
+
const contribution = createShellBotBackendContribution({
|
|
1530
|
+
state: { storage } as unknown as DurableObjectState,
|
|
1531
|
+
env: {} as never,
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
const page = await contribution.listRuns({
|
|
1535
|
+
schemaVersion: 1,
|
|
1536
|
+
conversationId: "user:primary:older",
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
expect(page.runs.map((run) => run.runId)).toEqual(["run-older"]);
|
|
1540
|
+
expect(page.page.truncated).toBe(false);
|
|
1541
|
+
});
|
|
1542
|
+
|
|
1543
|
+
test("keeps the visible chat reachable behind a page of automation", async () => {
|
|
1544
|
+
const storage = new MemoryStorage();
|
|
1545
|
+
await writeRunHistory(storage, [
|
|
1546
|
+
{ runId: "run-chat", sessionId: "user:primary" },
|
|
1547
|
+
...Array.from({ length: CLIENT_RUN_PAGE_LIMIT + 2 }, (_value, index) => ({
|
|
1548
|
+
runId: `run-automation-${index.toString().padStart(3, "0")}`,
|
|
1549
|
+
sessionId: "user:primary",
|
|
1550
|
+
turnType: "automation" as const,
|
|
1551
|
+
})),
|
|
1552
|
+
]);
|
|
1553
|
+
const contribution = createShellBotBackendContribution({
|
|
1554
|
+
state: { storage } as unknown as DurableObjectState,
|
|
1555
|
+
env: {} as never,
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
const page = await contribution.listRuns({
|
|
1559
|
+
schemaVersion: 1,
|
|
1560
|
+
conversationId: "user:primary",
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
expect(page.runs.map((run) => run.runId)).toEqual(["run-chat"]);
|
|
1564
|
+
});
|
|
1565
|
+
|
|
1566
|
+
test("offers a continuation when a scanned page selects nothing", async () => {
|
|
1567
|
+
const storage = new MemoryStorage();
|
|
1568
|
+
const buried = CLIENT_RUN_SCAN_LIMIT + CLIENT_RUN_PAGE_LIMIT;
|
|
1569
|
+
await writeRunHistory(storage, [
|
|
1570
|
+
{ runId: "run-older", sessionId: "user:primary:older" },
|
|
1571
|
+
...Array.from({ length: buried }, (_value, index) => ({
|
|
1572
|
+
runId: `run-newer-${index.toString().padStart(4, "0")}`,
|
|
1573
|
+
sessionId: "user:primary:newer",
|
|
1574
|
+
})),
|
|
1575
|
+
]);
|
|
1576
|
+
const contribution = createShellBotBackendContribution({
|
|
1577
|
+
state: { storage } as unknown as DurableObjectState,
|
|
1578
|
+
env: {} as never,
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1581
|
+
let page = await contribution.listRuns({
|
|
1582
|
+
schemaVersion: 1,
|
|
1583
|
+
conversationId: "user:primary:older",
|
|
1584
|
+
});
|
|
1585
|
+
// The budget stops the scan short of the match, so the page must be empty
|
|
1586
|
+
// *and* say where to resume. Answering `truncated: false` here is the
|
|
1587
|
+
// defect: it told the client the conversation had nothing in it.
|
|
1588
|
+
expect(page.runs).toEqual([]);
|
|
1589
|
+
expect(page.page.truncated).toBe(true);
|
|
1590
|
+
expect(page.page.nextCursor).toBeDefined();
|
|
1591
|
+
|
|
1592
|
+
const seen = new Set<string>();
|
|
1593
|
+
let requests = 0;
|
|
1594
|
+
while (page.runs.length === 0 && page.page.nextCursor) {
|
|
1595
|
+
if (seen.has(page.page.nextCursor)) {
|
|
1596
|
+
throw new Error("run list cursor stopped advancing");
|
|
1597
|
+
}
|
|
1598
|
+
seen.add(page.page.nextCursor);
|
|
1599
|
+
requests += 1;
|
|
1600
|
+
if (requests > 20) throw new Error("run list did not converge");
|
|
1601
|
+
page = await contribution.listRuns({
|
|
1602
|
+
schemaVersion: 1,
|
|
1603
|
+
conversationId: "user:primary:older",
|
|
1604
|
+
before: page.page.nextCursor,
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
expect(page.runs.map((run) => run.runId)).toEqual(["run-older"]);
|
|
1609
|
+
expect(page.page.truncated).toBe(false);
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1478
1612
|
test("pages large run history with bounded indexed reads and wire bytes", async () => {
|
|
1479
1613
|
const storage = new MemoryStorage();
|
|
1480
1614
|
const baseTime = Date.parse("2026-08-28T00:00:00.000Z");
|
package/src/backend.ts
CHANGED
|
@@ -404,6 +404,7 @@ import {
|
|
|
404
404
|
import {
|
|
405
405
|
CLIENT_RUN_LIST_MAX_BYTES,
|
|
406
406
|
CLIENT_RUN_PAGE_LIMIT,
|
|
407
|
+
CLIENT_RUN_SCAN_LIMIT,
|
|
407
408
|
clientRunListWireBytes,
|
|
408
409
|
createClientRunListV1,
|
|
409
410
|
createClientRunStopReceiptV1,
|
|
@@ -5833,7 +5834,7 @@ export class ShellBotBackendContribution {
|
|
|
5833
5834
|
|
|
5834
5835
|
const selected = new Map<string, { cursor?: string; run: ClientRunV1 }>();
|
|
5835
5836
|
if (activeRunId) {
|
|
5836
|
-
const active = await this.authority.
|
|
5837
|
+
const active = await this.authority.readStoredRunForDisplay(activeRunId);
|
|
5837
5838
|
// An automation firing occupies the object like any other run, and is
|
|
5838
5839
|
// still not part of the conversation: the visible transcript never
|
|
5839
5840
|
// shows one, running or settled.
|
|
@@ -5842,60 +5843,110 @@ export class ShellBotBackendContribution {
|
|
|
5842
5843
|
run: projectClientRunOrDegradedV1(active),
|
|
5843
5844
|
});
|
|
5844
5845
|
}
|
|
5845
|
-
|
|
5846
|
+
// The run index is global and the transcript is one conversation, so a
|
|
5847
|
+
// page of candidates is not a page of answers: 33 Turns of another
|
|
5848
|
+
// conversation, or of automation, used to come back as an empty,
|
|
5849
|
+
// *untruncated* page that told the client there was nothing older. The
|
|
5850
|
+
// scan cursor now advances over every candidate this call consumed,
|
|
5851
|
+
// whether or not it was kept, so a filtered-out page still says where to
|
|
5852
|
+
// resume; and the scan keeps reading batches, up to a budget, so the
|
|
5853
|
+
// common case answers in one request rather than making the client walk
|
|
5854
|
+
// the history a page at a time. Filtering reads run records only —
|
|
5855
|
+
// hydrating a Turn's journal is what selection costs, not what the scan
|
|
5856
|
+
// costs.
|
|
5846
5857
|
let stoppedEarly = false;
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
const
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5858
|
+
let exhausted = false;
|
|
5859
|
+
let scanCursor: string | undefined;
|
|
5860
|
+
let scanned = 0;
|
|
5861
|
+
let batch = candidates;
|
|
5862
|
+
for (;;) {
|
|
5863
|
+
const available = batch.slice(0, CLIENT_RUN_PAGE_LIMIT);
|
|
5864
|
+
const hasMore = batch.length > CLIENT_RUN_PAGE_LIMIT;
|
|
5865
|
+
for (const candidate of available) {
|
|
5866
|
+
if (selected.has(candidate.runId)) {
|
|
5867
|
+
const current = selected.get(candidate.runId)!;
|
|
5868
|
+
selected.set(candidate.runId, {
|
|
5869
|
+
...current,
|
|
5870
|
+
cursor: candidate.cursor,
|
|
5871
|
+
});
|
|
5872
|
+
scanCursor = candidate.cursor;
|
|
5873
|
+
continue;
|
|
5874
|
+
}
|
|
5875
|
+
const header = await this.authority.readRunHeader(candidate.runId);
|
|
5876
|
+
if (!header || !isVisibleRunV1(header) || !inConversation(header)) {
|
|
5877
|
+
scanCursor = candidate.cursor;
|
|
5878
|
+
continue;
|
|
5879
|
+
}
|
|
5880
|
+
const stored = await this.authority.readStoredRunForDisplay(
|
|
5881
|
+
candidate.runId,
|
|
5867
5882
|
);
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5883
|
+
if (!stored) {
|
|
5884
|
+
scanCursor = candidate.cursor;
|
|
5885
|
+
continue;
|
|
5886
|
+
}
|
|
5887
|
+
const projected = projectClientRunOrDegradedV1(stored);
|
|
5888
|
+
const tentative = [
|
|
5889
|
+
...selected.values(),
|
|
5890
|
+
{ cursor: candidate.cursor, run: projected },
|
|
5891
|
+
];
|
|
5892
|
+
const ordered = tentative
|
|
5893
|
+
.map((entry) => entry.run)
|
|
5894
|
+
.sort(
|
|
5895
|
+
(left, right) =>
|
|
5896
|
+
left.admittedAt.localeCompare(right.admittedAt) ||
|
|
5897
|
+
left.runId.localeCompare(right.runId),
|
|
5898
|
+
);
|
|
5899
|
+
const tentativePage = createClientRunListV1(ordered, {
|
|
5900
|
+
truncated: true,
|
|
5901
|
+
nextCursor: candidate.cursor,
|
|
5902
|
+
});
|
|
5903
|
+
const isNewestTerminal =
|
|
5904
|
+
![...selected.values()].some(
|
|
5905
|
+
(entry) =>
|
|
5906
|
+
entry.run.status === "completed" || entry.run.status === "failed",
|
|
5907
|
+
) &&
|
|
5908
|
+
(projected.status === "completed" || projected.status === "failed");
|
|
5909
|
+
if (
|
|
5910
|
+
selected.size >= CLIENT_RUN_PAGE_LIMIT ||
|
|
5911
|
+
(!isNewestTerminal &&
|
|
5912
|
+
clientRunListWireBytes(tentativePage) > CLIENT_RUN_LIST_MAX_BYTES)
|
|
5913
|
+
) {
|
|
5914
|
+
stoppedEarly = true;
|
|
5915
|
+
break;
|
|
5916
|
+
}
|
|
5917
|
+
selected.set(stored.runId, {
|
|
5918
|
+
cursor: candidate.cursor,
|
|
5919
|
+
run: projected,
|
|
5920
|
+
});
|
|
5921
|
+
scanCursor = candidate.cursor;
|
|
5922
|
+
}
|
|
5923
|
+
scanned += available.length;
|
|
5924
|
+
if (stoppedEarly) break;
|
|
5925
|
+
if (!hasMore) {
|
|
5926
|
+
exhausted = true;
|
|
5927
|
+
break;
|
|
5928
|
+
}
|
|
5929
|
+
if (selected.size >= CLIENT_RUN_PAGE_LIMIT) break;
|
|
5930
|
+
if (scanned >= CLIENT_RUN_SCAN_LIMIT || scanCursor === undefined) break;
|
|
5931
|
+
batch = await this.authority.listRunIndex({
|
|
5932
|
+
limit: CLIENT_RUN_PAGE_LIMIT + 1,
|
|
5933
|
+
before: scanCursor,
|
|
5871
5934
|
});
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
(entry) =>
|
|
5875
|
-
entry.run.status === "completed" || entry.run.status === "failed",
|
|
5876
|
-
) &&
|
|
5877
|
-
(projected.status === "completed" || projected.status === "failed");
|
|
5878
|
-
if (
|
|
5879
|
-
selected.size >= CLIENT_RUN_PAGE_LIMIT ||
|
|
5880
|
-
(!isNewestTerminal &&
|
|
5881
|
-
clientRunListWireBytes(tentativePage) > CLIENT_RUN_LIST_MAX_BYTES)
|
|
5882
|
-
) {
|
|
5883
|
-
stoppedEarly = true;
|
|
5935
|
+
if (batch.length === 0) {
|
|
5936
|
+
exhausted = true;
|
|
5884
5937
|
break;
|
|
5885
5938
|
}
|
|
5886
|
-
selected.set(stored.runId, { cursor: candidate.cursor, run: projected });
|
|
5887
5939
|
}
|
|
5888
5940
|
const orderedEntries = [...selected.values()].sort(
|
|
5889
5941
|
(left, right) =>
|
|
5890
5942
|
left.run.admittedAt.localeCompare(right.run.admittedAt) ||
|
|
5891
5943
|
left.run.runId.localeCompare(right.run.runId),
|
|
5892
5944
|
);
|
|
5893
|
-
const
|
|
5894
|
-
const truncated = stoppedEarly || candidates.length > CLIENT_RUN_PAGE_LIMIT;
|
|
5945
|
+
const truncated = !exhausted;
|
|
5895
5946
|
const page = createClientRunListV1(
|
|
5896
5947
|
orderedEntries.map((entry) => entry.run),
|
|
5897
|
-
truncated &&
|
|
5898
|
-
? { truncated: true, nextCursor:
|
|
5948
|
+
truncated && scanCursor
|
|
5949
|
+
? { truncated: true, nextCursor: scanCursor }
|
|
5899
5950
|
: { truncated: false },
|
|
5900
5951
|
// Announcements belong to the Session, not to a page of Turns, so only
|
|
5901
5952
|
// the newest page carries them.
|
|
@@ -22,6 +22,11 @@ import {
|
|
|
22
22
|
appletSourceFingerprintV1,
|
|
23
23
|
mostRecentlyChangedFileV1,
|
|
24
24
|
} from "./applets-client.js";
|
|
25
|
+
import {
|
|
26
|
+
appletIsBeingBuiltV1,
|
|
27
|
+
appletProgressToolsV1,
|
|
28
|
+
appletProgressV1,
|
|
29
|
+
} from "./applet-progress.js";
|
|
25
30
|
import { packageIframePagesForSlotV1 } from "./package-iframe-entries.js";
|
|
26
31
|
import PackageIframeHost from "./PackageIframeHost.vue";
|
|
27
32
|
|
|
@@ -46,6 +51,26 @@ const isRunning = computed(() => Boolean(web.value.activeRunId));
|
|
|
46
51
|
const canvasState = computed(() => web.value.appletCanvas);
|
|
47
52
|
const failure = computed(() => web.value.appletCanvasError);
|
|
48
53
|
|
|
54
|
+
/*
|
|
55
|
+
* What the Bot is doing to this Applet, in one line.
|
|
56
|
+
*
|
|
57
|
+
* A publish is minutes away from a create, and for all of it there is nothing
|
|
58
|
+
* to run. The panel used to answer that with one fixed line about the Applet
|
|
59
|
+
* not being live and a file list, which never changed and never said whether
|
|
60
|
+
* anything was happening. This is the sentence in between, projected from what
|
|
61
|
+
* the thread already shows: no new read, no new durable state.
|
|
62
|
+
*/
|
|
63
|
+
const progress = computed(() =>
|
|
64
|
+
appletProgressV1({
|
|
65
|
+
applet: applet.value ?? null,
|
|
66
|
+
source: source.value,
|
|
67
|
+
build: build.value,
|
|
68
|
+
tools: appletProgressToolsV1(web.value.messages),
|
|
69
|
+
running: isRunning.value,
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
const beingBuilt = computed(() => appletIsBeingBuiltV1(progress.value));
|
|
73
|
+
|
|
49
74
|
/** The Applets Package's right-panel page, when its Composition carries one. */
|
|
50
75
|
const panelPage = computed(
|
|
51
76
|
() => packageIframePagesForSlotV1(web.value.packageUi, RIGHT_PANEL_SLOT)[0],
|
|
@@ -212,8 +237,11 @@ async function clearFocus(): Promise<void> {
|
|
|
212
237
|
/></span>
|
|
213
238
|
<div class="applet-canvas-title">
|
|
214
239
|
<strong>{{ applet?.displayName ?? "Applet" }}</strong>
|
|
240
|
+
<!--
|
|
241
|
+
While it is being built the line under the name is the pinned
|
|
242
|
+
building view below, not a copy of it here.
|
|
243
|
+
-->
|
|
215
244
|
<small v-if="viewer">{{ generationLabel(viewer.generationId) }}</small>
|
|
216
|
-
<small v-else>Not published yet</small>
|
|
217
245
|
</div>
|
|
218
246
|
<div
|
|
219
247
|
v-if="canShowApp"
|
|
@@ -255,6 +283,39 @@ async function clearFocus(): Promise<void> {
|
|
|
255
283
|
/>
|
|
256
284
|
</header>
|
|
257
285
|
|
|
286
|
+
<!--
|
|
287
|
+
Where the work has got to, pinned under the header while the Applet is
|
|
288
|
+
still being built: one line a person can read, the reason it stopped when
|
|
289
|
+
it stopped, and the tail of whatever the check or the build printed —
|
|
290
|
+
which is the only thing that helps when the Bot is going round in circles
|
|
291
|
+
on a type error. It sits outside the scrolling column deliberately: a
|
|
292
|
+
person reading the source still wants to know what is happening to it.
|
|
293
|
+
-->
|
|
294
|
+
<section
|
|
295
|
+
v-if="applet && !loading && progress && beingBuilt"
|
|
296
|
+
class="applet-canvas-progress"
|
|
297
|
+
:class="{ 'applet-canvas-progress-wrong': Boolean(progress.failure) }"
|
|
298
|
+
data-testid="applet-canvas-progress"
|
|
299
|
+
role="status"
|
|
300
|
+
>
|
|
301
|
+
<p class="applet-canvas-progress-line">
|
|
302
|
+
<span
|
|
303
|
+
v-if="progress.working"
|
|
304
|
+
class="applet-canvas-progress-dot"
|
|
305
|
+
aria-hidden="true"
|
|
306
|
+
/>
|
|
307
|
+
<UiIcon v-else-if="progress.failure" name="close" size="sm" />
|
|
308
|
+
<span>{{ progress.label }}</span>
|
|
309
|
+
</p>
|
|
310
|
+
<p v-if="progress.failure" class="applet-canvas-progress-failure">
|
|
311
|
+
{{ progress.failure }}
|
|
312
|
+
</p>
|
|
313
|
+
<pre
|
|
314
|
+
v-if="progress.output"
|
|
315
|
+
class="applet-canvas-progress-output"
|
|
316
|
+
><code>{{ progress.output.join("\n") }}</code></pre>
|
|
317
|
+
</section>
|
|
318
|
+
|
|
258
319
|
<div class="applet-canvas-body">
|
|
259
320
|
<!-- Loading: the shape of what is coming, with a caption that says so. -->
|
|
260
321
|
<div v-if="loading" class="applet-canvas-loading">
|
|
@@ -297,21 +358,6 @@ async function clearFocus(): Promise<void> {
|
|
|
297
358
|
<span>This is taking longer than it should.</span>
|
|
298
359
|
<button type="button" @click="retry">Try again</button>
|
|
299
360
|
</div>
|
|
300
|
-
<p
|
|
301
|
-
v-if="build && build.status !== 'unknown'"
|
|
302
|
-
class="applet-canvas-build"
|
|
303
|
-
:class="`applet-canvas-build-${build.status}`"
|
|
304
|
-
>
|
|
305
|
-
<UiIcon
|
|
306
|
-
:name="build.status === 'passed' ? 'check' : 'close'"
|
|
307
|
-
size="sm"
|
|
308
|
-
/>
|
|
309
|
-
<span
|
|
310
|
-
>{{ build.command === "build" ? "Build" : "Check" }}
|
|
311
|
-
{{ build.status
|
|
312
|
-
}}{{ build.summary ? `: ${build.summary}` : "" }}</span
|
|
313
|
-
>
|
|
314
|
-
</p>
|
|
315
361
|
<div v-if="sortedFiles.length > 0" class="applet-canvas-files">
|
|
316
362
|
<button
|
|
317
363
|
v-for="file in sortedFiles"
|
|
@@ -573,28 +619,76 @@ async function clearFocus(): Promise<void> {
|
|
|
573
619
|
white-space: pre;
|
|
574
620
|
}
|
|
575
621
|
|
|
576
|
-
.
|
|
622
|
+
/* Where the work has got to: the panel's own header for the building state. */
|
|
623
|
+
.applet-canvas-progress {
|
|
624
|
+
display: flex;
|
|
625
|
+
flex: 0 0 auto;
|
|
626
|
+
flex-direction: column;
|
|
627
|
+
gap: 8px;
|
|
628
|
+
padding: 10px 12px;
|
|
629
|
+
border-bottom: 1px solid var(--frock-border);
|
|
630
|
+
background: var(--frock-surface-subtle);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.applet-canvas-progress-wrong {
|
|
634
|
+
border-bottom-color: var(--frock-danger-border);
|
|
635
|
+
background: var(--frock-danger-surface);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.applet-canvas-progress-line {
|
|
577
639
|
display: flex;
|
|
578
640
|
align-items: center;
|
|
579
|
-
gap:
|
|
641
|
+
gap: 8px;
|
|
580
642
|
margin: 0;
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
643
|
+
color: var(--frock-text);
|
|
644
|
+
font-size: var(--frock-text-sm);
|
|
645
|
+
font-weight: 600;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.applet-canvas-progress-wrong .applet-canvas-progress-line {
|
|
649
|
+
color: var(--frock-danger-text);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/* The same "something is happening" the thread uses, at the size of a word. */
|
|
653
|
+
.applet-canvas-progress-dot {
|
|
654
|
+
width: 8px;
|
|
655
|
+
height: 8px;
|
|
656
|
+
flex: 0 0 auto;
|
|
657
|
+
border-radius: 999px;
|
|
658
|
+
background: var(--frock-action-primary);
|
|
659
|
+
animation: applet-progress-pulse 1.4s ease-in-out infinite;
|
|
586
660
|
}
|
|
587
661
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
662
|
+
@keyframes applet-progress-pulse {
|
|
663
|
+
0%,
|
|
664
|
+
100% {
|
|
665
|
+
opacity: 0.35;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
50% {
|
|
669
|
+
opacity: 1;
|
|
670
|
+
}
|
|
592
671
|
}
|
|
593
672
|
|
|
594
|
-
.applet-canvas-
|
|
595
|
-
|
|
673
|
+
.applet-canvas-progress-failure {
|
|
674
|
+
margin: 0;
|
|
596
675
|
color: var(--frock-danger-text);
|
|
597
|
-
|
|
676
|
+
font-size: var(--frock-text-sm);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
.applet-canvas-progress-output {
|
|
680
|
+
max-height: 180px;
|
|
681
|
+
margin: 0;
|
|
682
|
+
overflow: auto;
|
|
683
|
+
padding: 8px 10px;
|
|
684
|
+
border: 1px solid var(--frock-border);
|
|
685
|
+
border-radius: var(--frock-radius-control);
|
|
686
|
+
color: var(--frock-text-muted);
|
|
687
|
+
background: var(--frock-surface);
|
|
688
|
+
font-family: var(--frock-font-mono);
|
|
689
|
+
font-size: var(--frock-text-xs);
|
|
690
|
+
line-height: var(--frock-leading-snug);
|
|
691
|
+
white-space: pre;
|
|
598
692
|
}
|
|
599
693
|
|
|
600
694
|
.applet-canvas-failure {
|
|
@@ -686,5 +780,9 @@ async function clearFocus(): Promise<void> {
|
|
|
686
780
|
.applet-app-leave-active {
|
|
687
781
|
transition: none;
|
|
688
782
|
}
|
|
783
|
+
|
|
784
|
+
.applet-canvas-progress-dot {
|
|
785
|
+
animation: none;
|
|
786
|
+
}
|
|
689
787
|
}
|
|
690
788
|
</style>
|