@frockbot/plugin-shell 0.3.10 → 0.3.12
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 +34 -32
- package/src/agent.test.ts +66 -0
- package/src/agent.ts +107 -2
- package/src/backend-configuration.test.ts +15 -9
- package/src/backend-package-catalog.ts +75 -26
- package/src/backend-runner.ts +19 -2
- package/src/backend.ts +118 -27
- package/src/client/FrockBotApp.vue +405 -134
- package/src/client/activity-trail.test.ts +205 -0
- package/src/client/activity-trail.ts +227 -0
- package/src/client/index.test.ts +25 -5
- package/src/client/index.ts +191 -47
- package/src/client/model-presentation.test.ts +3 -3
- package/src/client/no-bot-model-label.test.ts +7 -7
- package/src/client/skill-invocation.test.ts +34 -0
- package/src/client/skill-invocation.ts +22 -0
- package/src/client/styles.css +134 -89
- package/src/client/transcript-cache.test.ts +125 -0
- package/src/client/transcript-cache.ts +190 -0
- package/src/compaction-scheduler.test.ts +96 -0
- package/src/compaction-scheduler.ts +108 -0
- package/src/compaction-transcript.test.ts +174 -0
- package/src/compaction.test.ts +596 -0
- package/src/compaction.ts +539 -0
- package/src/focus.test.ts +222 -0
- package/src/focus.ts +93 -0
- package/src/history.ts +86 -8
- package/src/legacy-frock-model-id.test.ts +148 -0
- package/src/notification-id.test.ts +26 -0
- package/src/notification-id.ts +0 -0
- package/src/run-protocol.test.ts +37 -0
- package/src/run-protocol.ts +148 -38
- package/src/settings-links.test.ts +8 -2
- package/src/settings-links.ts +17 -2
- package/src/shared.ts +36 -0
- package/src/unread.ts +23 -1
- package/tsconfig.json +1 -2
package/src/run-protocol.ts
CHANGED
|
@@ -5,7 +5,10 @@ import {
|
|
|
5
5
|
type SessionEvent,
|
|
6
6
|
type SkillRefV1,
|
|
7
7
|
} from "@frockbot/kernel-contracts";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
isPublicIdentifier,
|
|
10
|
+
isRpcIdentifier,
|
|
11
|
+
} from "@frockbot/configuration-core";
|
|
9
12
|
import { decodeRunCursorV1, RUN_CURSOR_PATTERN } from "./run-cursor.js";
|
|
10
13
|
export { decodeRunCursorV1, RUN_CURSOR_PATTERN };
|
|
11
14
|
import type {
|
|
@@ -241,14 +244,30 @@ export interface ClientRunPageV1 {
|
|
|
241
244
|
* A durable Session event that belongs to no Turn. The WebUI renders it as a
|
|
242
245
|
* system line in the conversation.
|
|
243
246
|
*/
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
247
|
+
/**
|
|
248
|
+
* A session-level line in the transcript that belongs to neither party.
|
|
249
|
+
*
|
|
250
|
+
* `conversation/compacted` is ADR 0030's one user-visible surface: the earlier
|
|
251
|
+
* Turns are still there and still readable, and this says plainly that the
|
|
252
|
+
* model now carries a summary of them instead of the Turns themselves. ADR
|
|
253
|
+
* 0027's "not summarised" notice still stands where Turns were genuinely
|
|
254
|
+
* evicted, so the two never claim each other's ground.
|
|
255
|
+
*/
|
|
256
|
+
export type ClientAnnouncementV1 =
|
|
257
|
+
| {
|
|
258
|
+
type: "bot/renamed";
|
|
259
|
+
announcementId: string;
|
|
260
|
+
at: string;
|
|
261
|
+
from: string;
|
|
262
|
+
to: string;
|
|
263
|
+
namedBy: "user" | "bot";
|
|
264
|
+
}
|
|
265
|
+
| {
|
|
266
|
+
type: "conversation/compacted";
|
|
267
|
+
announcementId: string;
|
|
268
|
+
at: string;
|
|
269
|
+
throughTurn: number;
|
|
270
|
+
};
|
|
252
271
|
|
|
253
272
|
export interface ClientRunListV1 {
|
|
254
273
|
schemaVersion: 1;
|
|
@@ -283,6 +302,19 @@ export interface ClientConversationListV1 {
|
|
|
283
302
|
conversations: ClientConversationV1[];
|
|
284
303
|
}
|
|
285
304
|
|
|
305
|
+
/**
|
|
306
|
+
* The answer to "start a new conversation": the list, or the reason not now.
|
|
307
|
+
*
|
|
308
|
+
* A refusal is a value, not an exception. The request crosses a Durable Object
|
|
309
|
+
* boundary and a Worker boundary to get here, and an exception that crosses
|
|
310
|
+
* either is logged by workerd as `Uncaught Error` — the log then showed the
|
|
311
|
+
* isolate going down with a broken pipe behind it. Carrying the refusal as
|
|
312
|
+
* data means the only thing that reaches the client is the 409 it expects.
|
|
313
|
+
*/
|
|
314
|
+
export type ClientConversationOutcomeV1 =
|
|
315
|
+
| ({ status: "started" } & ClientConversationListV1)
|
|
316
|
+
| { status: "refused"; schemaVersion: 1; reason: string };
|
|
317
|
+
|
|
286
318
|
export function decodeClientConversationListV1(
|
|
287
319
|
input: unknown,
|
|
288
320
|
): ClientConversationListV1 {
|
|
@@ -980,24 +1012,74 @@ export function createClientRunListV1(
|
|
|
980
1012
|
const MAX_ANNOUNCEMENTS = 64;
|
|
981
1013
|
const MAX_ANNOUNCEMENT_NAME_BYTES = 400;
|
|
982
1014
|
|
|
983
|
-
/**
|
|
1015
|
+
/**
|
|
1016
|
+
* Where each Turn ended, by Turn number.
|
|
1017
|
+
*
|
|
1018
|
+
* A compaction is written at the end of the Turn that crossed the threshold,
|
|
1019
|
+
* which is the *newest* Turn — so its own timestamp would place its marker at
|
|
1020
|
+
* the bottom of the thread, far from the range it describes. The boundary it
|
|
1021
|
+
* actually names is the end of `throughTurn`, and that is what the marker is
|
|
1022
|
+
* dated with.
|
|
1023
|
+
*/
|
|
1024
|
+
function turnEndTimestampsV1(
|
|
1025
|
+
session: readonly SessionEvent[],
|
|
1026
|
+
): Map<number, string> {
|
|
1027
|
+
const ends = new Map<number, string>();
|
|
1028
|
+
for (const event of session) {
|
|
1029
|
+
if (event.type === "turn/end") ends.set(event.turn, event.timestamp);
|
|
1030
|
+
}
|
|
1031
|
+
return ends;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Projects the Bot's durable announcement events onto the wire.
|
|
1036
|
+
*
|
|
1037
|
+
* `session` is the conversation's own log, used only to date a compaction
|
|
1038
|
+
* marker at the boundary it covers. Omitting it dates the marker by when the
|
|
1039
|
+
* compaction was written, which is where it used to sit.
|
|
1040
|
+
*/
|
|
984
1041
|
export function projectClientAnnouncementsV1(
|
|
985
1042
|
events: readonly SessionEvent[],
|
|
1043
|
+
session: readonly SessionEvent[] = events,
|
|
986
1044
|
): ClientAnnouncementV1[] {
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1045
|
+
const turnEnds = turnEndTimestampsV1(session);
|
|
1046
|
+
return events.flatMap((event): ClientAnnouncementV1[] => {
|
|
1047
|
+
if (event.type === "bot/renamed") {
|
|
1048
|
+
return [
|
|
1049
|
+
{
|
|
1050
|
+
type: "bot/renamed" as const,
|
|
1051
|
+
announcementId: `announcement-${event.seq}`,
|
|
1052
|
+
at: truncate(event.timestamp, MAX_TIMESTAMP_LENGTH),
|
|
1053
|
+
from: truncateWireString(event.from, MAX_ANNOUNCEMENT_NAME_BYTES),
|
|
1054
|
+
to: truncateWireString(event.to, MAX_ANNOUNCEMENT_NAME_BYTES),
|
|
1055
|
+
namedBy: event.namedBy,
|
|
1056
|
+
},
|
|
1057
|
+
];
|
|
1058
|
+
}
|
|
1059
|
+
if (event.type === "conversation/compacted") {
|
|
1060
|
+
// The summary itself is deliberately not on the wire. A person can read
|
|
1061
|
+
// every Turn it covers, unchanged, immediately above this line; the
|
|
1062
|
+
// summary is what the model carries, and it belongs to the audit view.
|
|
1063
|
+
return [
|
|
1064
|
+
{
|
|
1065
|
+
type: "conversation/compacted" as const,
|
|
1066
|
+
// A distinct prefix: a compaction is numbered by the session log and
|
|
1067
|
+
// a rename by this Bot's announcement log, and the two counters would
|
|
1068
|
+
// otherwise collide on an id the client upserts by.
|
|
1069
|
+
announcementId: `compaction-${event.seq}`,
|
|
1070
|
+
// Dated where the covered range ends, not when the summariser ran,
|
|
1071
|
+
// so the marker sits between the last compacted Turn and the first
|
|
1072
|
+
// verbatim one and stays there as newer Turns arrive.
|
|
1073
|
+
at: truncate(
|
|
1074
|
+
turnEnds.get(event.throughTurn) ?? event.timestamp,
|
|
1075
|
+
MAX_TIMESTAMP_LENGTH,
|
|
1076
|
+
),
|
|
1077
|
+
throughTurn: event.throughTurn,
|
|
1078
|
+
},
|
|
1079
|
+
];
|
|
1080
|
+
}
|
|
1081
|
+
return [];
|
|
1082
|
+
});
|
|
1001
1083
|
}
|
|
1002
1084
|
|
|
1003
1085
|
export function clientRunListWireBytes(value: ClientRunListV1): number {
|
|
@@ -1648,7 +1730,15 @@ export function decodeClientNotificationAcknowledgementCommandV1(
|
|
|
1648
1730
|
MAX_RUN_ID_LENGTH,
|
|
1649
1731
|
"notification acknowledgement command",
|
|
1650
1732
|
);
|
|
1651
|
-
|
|
1733
|
+
// `isRpcIdentifier`, not `isPublicIdentifier`: the same bounded alphabet
|
|
1734
|
+
// plus `:` and `@`. New ids are minted through `notificationIdV1` and carry
|
|
1735
|
+
// neither, but Bots in the field are already holding notifications whose ids
|
|
1736
|
+
// were interpolated by hand — `composition-failure:<generationId>:<attempt>`
|
|
1737
|
+
// and three more like it. Under the stricter pattern those could never be
|
|
1738
|
+
// acknowledged, so the client retried them forever and the Bot answered 400
|
|
1739
|
+
// on every poll for the rest of its life. Accepting them here is what lets
|
|
1740
|
+
// an already-wedged Bot recover without a storage sweep.
|
|
1741
|
+
if (!isRpcIdentifier(notificationId)) {
|
|
1652
1742
|
throw new Error(
|
|
1653
1743
|
"notification acknowledgement command.notificationId is invalid",
|
|
1654
1744
|
);
|
|
@@ -1817,14 +1907,19 @@ export function decodeClientRunLookupV1(input: unknown): ClientRunLookup {
|
|
|
1817
1907
|
|
|
1818
1908
|
function decodeAnnouncement(value: unknown): ClientAnnouncementV1 {
|
|
1819
1909
|
const announcement = record(value, "run list.announcement");
|
|
1910
|
+
if (
|
|
1911
|
+
announcement.type !== "bot/renamed" &&
|
|
1912
|
+
announcement.type !== "conversation/compacted"
|
|
1913
|
+
) {
|
|
1914
|
+
throw new Error("run list.announcement.type is invalid");
|
|
1915
|
+
}
|
|
1820
1916
|
exactKeys(
|
|
1821
1917
|
announcement,
|
|
1822
|
-
|
|
1918
|
+
announcement.type === "conversation/compacted"
|
|
1919
|
+
? ["type", "announcementId", "at", "throughTurn"]
|
|
1920
|
+
: ["type", "announcementId", "at", "from", "to", "namedBy"],
|
|
1823
1921
|
"run list.announcement",
|
|
1824
1922
|
);
|
|
1825
|
-
if (announcement.type !== "bot/renamed") {
|
|
1826
|
-
throw new Error("run list.announcement.type is invalid");
|
|
1827
|
-
}
|
|
1828
1923
|
const at = string(
|
|
1829
1924
|
announcement,
|
|
1830
1925
|
"at",
|
|
@@ -1834,20 +1929,35 @@ function decodeAnnouncement(value: unknown): ClientAnnouncementV1 {
|
|
|
1834
1929
|
if (!Number.isFinite(Date.parse(at))) {
|
|
1835
1930
|
throw new Error("run list.announcement.at is invalid");
|
|
1836
1931
|
}
|
|
1932
|
+
const announcementId = publicEventId(
|
|
1933
|
+
string(
|
|
1934
|
+
announcement,
|
|
1935
|
+
"announcementId",
|
|
1936
|
+
MAX_EVENT_ID_LENGTH,
|
|
1937
|
+
"run list.announcement",
|
|
1938
|
+
),
|
|
1939
|
+
"run list.announcement.announcementId",
|
|
1940
|
+
);
|
|
1941
|
+
if (announcement.type === "conversation/compacted") {
|
|
1942
|
+
if (
|
|
1943
|
+
!Number.isSafeInteger(announcement.throughTurn) ||
|
|
1944
|
+
(announcement.throughTurn as number) < 1
|
|
1945
|
+
) {
|
|
1946
|
+
throw new Error("run list.announcement.throughTurn is invalid");
|
|
1947
|
+
}
|
|
1948
|
+
return {
|
|
1949
|
+
type: "conversation/compacted",
|
|
1950
|
+
announcementId,
|
|
1951
|
+
at,
|
|
1952
|
+
throughTurn: announcement.throughTurn as number,
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1837
1955
|
if (announcement.namedBy !== "user" && announcement.namedBy !== "bot") {
|
|
1838
1956
|
throw new Error("run list.announcement.namedBy is invalid");
|
|
1839
1957
|
}
|
|
1840
1958
|
return {
|
|
1841
1959
|
type: "bot/renamed",
|
|
1842
|
-
announcementId
|
|
1843
|
-
string(
|
|
1844
|
-
announcement,
|
|
1845
|
-
"announcementId",
|
|
1846
|
-
MAX_EVENT_ID_LENGTH,
|
|
1847
|
-
"run list.announcement",
|
|
1848
|
-
),
|
|
1849
|
-
"run list.announcement.announcementId",
|
|
1850
|
-
),
|
|
1960
|
+
announcementId,
|
|
1851
1961
|
at,
|
|
1852
1962
|
from: wireString(
|
|
1853
1963
|
announcement,
|
|
@@ -62,10 +62,16 @@ describe("settings link scheme", () => {
|
|
|
62
62
|
});
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
test("
|
|
65
|
+
test("a fragment belonging to another surface opens that surface", () => {
|
|
66
|
+
// The row is the specific request; the query parameter is where the link
|
|
67
|
+
// was written from, and rows move between surfaces.
|
|
66
68
|
expect(
|
|
67
69
|
decodeSettingsLinkV1("/?settings=bot-settings#bot-info-computer"),
|
|
68
|
-
).toEqual({ surface: "bot-
|
|
70
|
+
).toEqual({ surface: "bot-panel", anchor: "bot-info-computer" });
|
|
71
|
+
expect(decodeSettingsLinkV1("/?settings=bot-panel#bot-routines")).toEqual({
|
|
72
|
+
surface: "bot-settings",
|
|
73
|
+
anchor: "bot-routines",
|
|
74
|
+
});
|
|
69
75
|
});
|
|
70
76
|
|
|
71
77
|
test("drops an anchor nobody registered", () => {
|
package/src/settings-links.ts
CHANGED
|
@@ -81,6 +81,12 @@ export const SETTINGS_ANCHORS_V1: readonly SettingsAnchorV1[] = [
|
|
|
81
81
|
label: "Label",
|
|
82
82
|
scope: "bot",
|
|
83
83
|
},
|
|
84
|
+
{
|
|
85
|
+
anchor: "bot-pinned",
|
|
86
|
+
surface: "bot-settings",
|
|
87
|
+
label: "Pinned",
|
|
88
|
+
scope: "bot",
|
|
89
|
+
},
|
|
84
90
|
{
|
|
85
91
|
anchor: "bot-description",
|
|
86
92
|
surface: "bot-settings",
|
|
@@ -269,9 +275,18 @@ export function decodeSettingsLinkV1(
|
|
|
269
275
|
const fragment = decodeURIComponent(url.hash.replace(/^#/u, ""));
|
|
270
276
|
const entry = fragment ? settingsAnchorV1(fragment) : undefined;
|
|
271
277
|
const botId = url.searchParams.get("bot") ?? undefined;
|
|
278
|
+
/*
|
|
279
|
+
* A fragment naming a row that lives somewhere else is still a request for
|
|
280
|
+
* that row, so the anchor decides which surface opens. Dropping it instead
|
|
281
|
+
* is what made `?settings=bot-panel#bot-routines` open the panel and then
|
|
282
|
+
* sit there: a link that resolves to nothing visible reads as a broken app,
|
|
283
|
+
* and a row moving between surfaces is exactly what these links have to
|
|
284
|
+
* survive. A fragment nobody registered still opens the named surface with
|
|
285
|
+
* no row.
|
|
286
|
+
*/
|
|
272
287
|
return {
|
|
273
|
-
surface,
|
|
274
|
-
...(entry
|
|
288
|
+
surface: entry ? entry.surface : surface,
|
|
289
|
+
...(entry ? { anchor: entry.anchor } : {}),
|
|
275
290
|
...(botId ? { botId } : {}),
|
|
276
291
|
};
|
|
277
292
|
}
|
package/src/shared.ts
CHANGED
|
@@ -222,6 +222,29 @@ export function withoutConnectionReturnV1(search: string): string {
|
|
|
222
222
|
return rest ? `?${rest}` : "";
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
/**
|
|
226
|
+
* What the shell holds of the conversations the User is not looking at, as
|
|
227
|
+
* everything outside the shell may address it.
|
|
228
|
+
*
|
|
229
|
+
* Two callers: the thread, which records and reads back where the reader had
|
|
230
|
+
* each Bot scrolled, and any surface that changes a Bot enough that its held
|
|
231
|
+
* transcript would be a lie rather than merely old — archive, delete, and a
|
|
232
|
+
* change of signed-in User.
|
|
233
|
+
*/
|
|
234
|
+
export interface TranscriptMemoryV1 {
|
|
235
|
+
/** Records where the reader had this Bot's thread. */
|
|
236
|
+
rememberViewport(
|
|
237
|
+
botId: string,
|
|
238
|
+
viewport: { scrollTop: number; pinnedToLatest: boolean },
|
|
239
|
+
): void;
|
|
240
|
+
/** Where the reader had this Bot's thread, if it is still held. */
|
|
241
|
+
viewportFor(
|
|
242
|
+
botId: string,
|
|
243
|
+
): { scrollTop: number; pinnedToLatest: boolean } | undefined;
|
|
244
|
+
/** Drops what is held for one Bot, or — with no argument — for every Bot. */
|
|
245
|
+
forget(botId?: string): void;
|
|
246
|
+
}
|
|
247
|
+
|
|
225
248
|
export interface FrockBotWebData {
|
|
226
249
|
connection: WebConnection;
|
|
227
250
|
modelLabel: string;
|
|
@@ -249,6 +272,14 @@ export interface FrockBotWebData {
|
|
|
249
272
|
runningRunId?: string;
|
|
250
273
|
activeRun?: WebActiveRun;
|
|
251
274
|
error?: string;
|
|
275
|
+
/**
|
|
276
|
+
* The Bot list could not be read, so "no Bot is open" means "we do not know
|
|
277
|
+
* yet" rather than "this account has none". Set by whichever Package owns
|
|
278
|
+
* the list. Without it a transport failure reads as data loss: the
|
|
279
|
+
* first-run empty state offering to add a first Bot to a User who has
|
|
280
|
+
* several is the worst thing this surface can say.
|
|
281
|
+
*/
|
|
282
|
+
botsUnavailable?: boolean;
|
|
252
283
|
botSettings?: BotSettingsViewV1;
|
|
253
284
|
userSettings?: UserSettingsViewV1;
|
|
254
285
|
pluginCatalog: PluginCatalogItem[];
|
|
@@ -328,6 +359,11 @@ export interface FrockBotWebData {
|
|
|
328
359
|
* cancelled or failed grant is reported rather than silently discarded.
|
|
329
360
|
*/
|
|
330
361
|
connectionReturn?: ConnectionReturnV1;
|
|
362
|
+
/**
|
|
363
|
+
* The conversations this client is still holding, so switching back to a
|
|
364
|
+
* Bot is a paint rather than a reload.
|
|
365
|
+
*/
|
|
366
|
+
transcripts: TranscriptMemoryV1;
|
|
331
367
|
selectBot(botId: string): Promise<void>;
|
|
332
368
|
loadBotSettings(): Promise<void>;
|
|
333
369
|
saveBotProfile(profile: BotProfile): Promise<void>;
|
package/src/unread.ts
CHANGED
|
@@ -389,6 +389,15 @@ export interface BotUnreadViewV1 {
|
|
|
389
389
|
lastViewedAt?: string;
|
|
390
390
|
/** Latest settled assistant/user line, projected for the sidebar only. */
|
|
391
391
|
lastMessage?: SidebarMessagePreviewV1;
|
|
392
|
+
/**
|
|
393
|
+
* Whether this Bot has a Turn running right now.
|
|
394
|
+
*
|
|
395
|
+
* The sidebar draws it as an activity ring on the row's avatar, so somebody
|
|
396
|
+
* reading one conversation can see another Bot still working rather than
|
|
397
|
+
* assuming it stalled. Optional: a view a client older than the Bot decodes,
|
|
398
|
+
* or one stored before this existed, simply draws no ring.
|
|
399
|
+
*/
|
|
400
|
+
working?: boolean;
|
|
392
401
|
}
|
|
393
402
|
|
|
394
403
|
export interface BotUnreadDirectoryViewV1 {
|
|
@@ -414,6 +423,8 @@ export function projectBotUnreadViewV1(
|
|
|
414
423
|
* so it counts here even though the firing that produced it does not.
|
|
415
424
|
*/
|
|
416
425
|
automationFailures = 0,
|
|
426
|
+
/** True while a Turn of this Bot's is running. Drawn as the row's ring. */
|
|
427
|
+
working = false,
|
|
417
428
|
): BotUnreadViewV1 {
|
|
418
429
|
const ceiling = state.lastActivityCursor;
|
|
419
430
|
let counted = Math.max(0, automationFailures);
|
|
@@ -449,6 +460,7 @@ export function projectBotUnreadViewV1(
|
|
|
449
460
|
? {}
|
|
450
461
|
: { lastViewedAt: state.lastViewedAt }),
|
|
451
462
|
...(lastMessage === undefined ? {} : { lastMessage }),
|
|
463
|
+
...(working ? { working: true } : {}),
|
|
452
464
|
};
|
|
453
465
|
}
|
|
454
466
|
|
|
@@ -528,7 +540,13 @@ function decodeBotUnreadViewV1(input: unknown): BotUnreadViewV1 {
|
|
|
528
540
|
exactKeys(
|
|
529
541
|
value,
|
|
530
542
|
["schemaVersion", "botId", "count", "capped", "unread", "manuallyUnread"],
|
|
531
|
-
[
|
|
543
|
+
[
|
|
544
|
+
"lastActivityCursor",
|
|
545
|
+
"lastActivityAt",
|
|
546
|
+
"lastViewedAt",
|
|
547
|
+
"lastMessage",
|
|
548
|
+
"working",
|
|
549
|
+
],
|
|
532
550
|
"unread view",
|
|
533
551
|
);
|
|
534
552
|
if (value.schemaVersion !== 1) {
|
|
@@ -563,6 +581,9 @@ function decodeBotUnreadViewV1(input: unknown): BotUnreadViewV1 {
|
|
|
563
581
|
);
|
|
564
582
|
const lastViewedAt = optionalTimestamp(value, "lastViewedAt", "unread view");
|
|
565
583
|
const lastMessage = optionalSidebarMessagePreviewV1(value.lastMessage);
|
|
584
|
+
if (value.working !== undefined && typeof value.working !== "boolean") {
|
|
585
|
+
throw new UnreadDecodeError("unread view working is invalid");
|
|
586
|
+
}
|
|
566
587
|
return {
|
|
567
588
|
schemaVersion: 1,
|
|
568
589
|
botId: value.botId,
|
|
@@ -574,6 +595,7 @@ function decodeBotUnreadViewV1(input: unknown): BotUnreadViewV1 {
|
|
|
574
595
|
...(lastActivityAt === undefined ? {} : { lastActivityAt }),
|
|
575
596
|
...(lastViewedAt === undefined ? {} : { lastViewedAt }),
|
|
576
597
|
...(lastMessage === undefined ? {} : { lastMessage }),
|
|
598
|
+
...(value.working === true ? { working: true } : {}),
|
|
577
599
|
};
|
|
578
600
|
}
|
|
579
601
|
|
package/tsconfig.json
CHANGED
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
"module": "ESNext",
|
|
5
5
|
"moduleResolution": "Bundler",
|
|
6
6
|
"allowImportingTsExtensions": true,
|
|
7
|
-
"baseUrl": ".",
|
|
8
7
|
"paths": {
|
|
9
|
-
"@cordisjs/client": ["src/client/cordis-client-shim.d.ts"]
|
|
8
|
+
"@cordisjs/client": ["./src/client/cordis-client-shim.d.ts"]
|
|
10
9
|
},
|
|
11
10
|
"strict": true,
|
|
12
11
|
"noEmit": true,
|