@frockbot/plugin-shell 0.3.11 → 0.3.13

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.
Files changed (42) hide show
  1. package/package.json +35 -33
  2. package/src/agent.test.ts +78 -0
  3. package/src/agent.ts +130 -2
  4. package/src/backend-configuration.test.ts +26 -26
  5. package/src/backend-recovery-integration.test.ts +10 -10
  6. package/src/backend-runner.ts +19 -2
  7. package/src/backend.ts +85 -18
  8. package/src/client/AppletCanvas.vue +19 -6
  9. package/src/client/FrockBotApp.vue +405 -75
  10. package/src/client/activity-trail.test.ts +205 -0
  11. package/src/client/activity-trail.ts +227 -0
  12. package/src/client/applets-client.test.ts +62 -0
  13. package/src/client/applets-client.ts +19 -0
  14. package/src/client/index.test.ts +128 -21
  15. package/src/client/index.ts +359 -114
  16. package/src/client/model-presentation.test.ts +3 -3
  17. package/src/client/no-bot-model-label.test.ts +7 -7
  18. package/src/client/skill-invocation.test.ts +34 -0
  19. package/src/client/skill-invocation.ts +22 -0
  20. package/src/client/styles.css +69 -19
  21. package/src/client/transcript-cache.test.ts +125 -0
  22. package/src/client/transcript-cache.ts +190 -0
  23. package/src/compaction-scheduler.test.ts +96 -0
  24. package/src/compaction-scheduler.ts +108 -0
  25. package/src/compaction-transcript.test.ts +174 -0
  26. package/src/compaction.test.ts +596 -0
  27. package/src/compaction.ts +539 -0
  28. package/src/focus.test.ts +222 -0
  29. package/src/focus.ts +93 -0
  30. package/src/history.ts +86 -8
  31. package/src/legacy-frock-model-id.test.ts +148 -0
  32. package/src/notification-id.ts +0 -0
  33. package/src/run-failure-copy.test.ts +150 -0
  34. package/src/run-failure-copy.ts +110 -0
  35. package/src/run-protocol.test.ts +50 -7
  36. package/src/run-protocol.ts +152 -43
  37. package/src/settings-links.test.ts +8 -2
  38. package/src/settings-links.ts +11 -2
  39. package/src/shared.ts +36 -0
  40. package/tsconfig.json +1 -2
  41. package/src/client/activity-ring.test.ts +0 -89
  42. package/src/client/activity-ring.ts +0 -94
@@ -275,9 +275,18 @@ export function decodeSettingsLinkV1(
275
275
  const fragment = decodeURIComponent(url.hash.replace(/^#/u, ""));
276
276
  const entry = fragment ? settingsAnchorV1(fragment) : undefined;
277
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
+ */
278
287
  return {
279
- surface,
280
- ...(entry && entry.surface === surface ? { anchor: entry.anchor } : {}),
288
+ surface: entry ? entry.surface : surface,
289
+ ...(entry ? { anchor: entry.anchor } : {}),
281
290
  ...(botId ? { botId } : {}),
282
291
  };
283
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/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,
@@ -1,89 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import {
3
- ACTIVITY_RING_MAX_LAPS_V1,
4
- ACTIVITY_RING_SEGMENTS_V1,
5
- activityRingV1,
6
- type ActivityStepStatusV1,
7
- } from "./activity-ring.js";
8
-
9
- /** `count` settled steps, plus whatever is still in flight. */
10
- function steps(settled: number, inFlight = 0): ActivityStepStatusV1[] {
11
- return [
12
- ...Array.from<ActivityStepStatusV1>({ length: settled }).fill("completed"),
13
- ...Array.from<ActivityStepStatusV1>({ length: inFlight }).fill("running"),
14
- ];
15
- }
16
-
17
- describe("the avatar's activity ring", () => {
18
- test("a Turn with nothing done yet still shows a ring", () => {
19
- // Liveness before progress: the ring is the answer to "is it paused?", so
20
- // it is drawn from the first moment of the Turn, empty and pulsing.
21
- const ring = activityRingV1({ toolStatuses: [], status: "streaming" });
22
- expect(ring.active).toBe(true);
23
- expect(ring.running).toBe(true);
24
- expect(ring.filled).toBe(0);
25
- expect(ring.progress).toBe(0);
26
- });
27
-
28
- test("a step that is still running has not ticked the ring", () => {
29
- const ring = activityRingV1({
30
- toolStatuses: steps(2, 1),
31
- status: "streaming",
32
- });
33
- expect(ring.steps).toBe(2);
34
- expect(ring.filled).toBe(2);
35
- });
36
-
37
- test("a failed step ticks like any other — it is a step that happened", () => {
38
- const ring = activityRingV1({
39
- toolStatuses: ["completed", "failed"],
40
- status: "streaming",
41
- });
42
- expect(ring.filled).toBe(2);
43
- });
44
-
45
- test("each settled step fills one more segment of the lap", () => {
46
- for (let settled = 0; settled < ACTIVITY_RING_SEGMENTS_V1; settled += 1) {
47
- const ring = activityRingV1({
48
- toolStatuses: steps(settled),
49
- status: "streaming",
50
- });
51
- expect(ring.filled).toBe(settled);
52
- expect(ring.progress).toBeCloseTo(settled / ACTIVITY_RING_SEGMENTS_V1);
53
- expect(ring.laps).toBe(0);
54
- }
55
- });
56
-
57
- test("a full lap leaves a faint ring behind and starts again", () => {
58
- const ring = activityRingV1({
59
- toolStatuses: steps(ACTIVITY_RING_SEGMENTS_V1 + 1),
60
- status: "streaming",
61
- });
62
- expect(ring.laps).toBe(1);
63
- expect(ring.filled).toBe(1);
64
- });
65
-
66
- test("a Turn of forty tool calls never grows a fortieth ring", () => {
67
- // The bound is the point: the ring reports that work is happening, not how
68
- // much, so past the last lap it loops inside the strokes it already has.
69
- const ring = activityRingV1({
70
- toolStatuses: steps(40),
71
- status: "streaming",
72
- });
73
- expect(ring.laps).toBe(ACTIVITY_RING_MAX_LAPS_V1);
74
- expect(ring.filled).toBeLessThan(ACTIVITY_RING_SEGMENTS_V1);
75
- });
76
-
77
- test("a settled Turn keeps no ring, however it ended", () => {
78
- for (const status of [
79
- "completed",
80
- "failed",
81
- "interrupted",
82
- "reconciliation-required",
83
- ] as const) {
84
- const ring = activityRingV1({ toolStatuses: steps(3), status });
85
- expect(ring.active).toBe(false);
86
- expect(ring.running).toBe(false);
87
- }
88
- });
89
- });
@@ -1,94 +0,0 @@
1
- /**
2
- * The activity ring: how much a running Turn has done, without saying so.
3
- *
4
- * A Turn can spend a minute making tool calls, and the thread used to answer
5
- * that with either nothing or a list of tool names. Neither is right — the
6
- * first reads as a paused app, the second puts the model's plumbing into a
7
- * conversation. The ring is the third answer: a stroke around the Bot's avatar
8
- * that pulses while the Turn runs and advances one segment for every step that
9
- * settles, so a person sees liveness and rough progress and reads no words at
10
- * all.
11
- *
12
- * This module is the whole rule, kept out of the component so it is testable
13
- * without mounting Vue.
14
- */
15
-
16
- /** Segments in one lap of the ring. A step fills one. */
17
- export const ACTIVITY_RING_SEGMENTS_V1 = 8;
18
-
19
- /**
20
- * How many laps the ring will draw at once. A Turn making forty tool calls
21
- * must not grow a fortieth ring, so the lap beyond the first is the last one:
22
- * past it the ring keeps looping inside those two strokes.
23
- */
24
- export const ACTIVITY_RING_MAX_LAPS_V1 = 1;
25
-
26
- /** A step's state, as the Turn's tool activity reports it. */
27
- export type ActivityStepStatusV1 = "running" | "completed" | "failed";
28
-
29
- /**
30
- * A Turn's state, as the thread's message carries it.
31
- *
32
- * Deliberately the whole string rather than the union: only `streaming` means
33
- * the Turn is still going, and every other value — including one a newer Bot
34
- * invents — is an ending. The ring is drawn off that one positive test, so it
35
- * cannot be left spinning by a status this file has never heard of.
36
- */
37
- export type ActivityTurnStatusV1 = string;
38
-
39
- export interface ActivityRingViewV1 {
40
- /** Whether the ring is drawn at all. A settled Turn keeps none. */
41
- active: boolean;
42
- /** Whether the ring pulses. False once the Turn has settled. */
43
- running: boolean;
44
- /** Steps that have settled in this Turn. Unbounded, and never rendered. */
45
- steps: number;
46
- /** Filled segments of the current lap, `0 … ACTIVITY_RING_SEGMENTS_V1`. */
47
- filled: number;
48
- /** Faint laps completed behind the live stroke, capped. */
49
- laps: number;
50
- /** The fraction of the circumference the live stroke draws, `0 … 1`. */
51
- progress: number;
52
- }
53
-
54
- /**
55
- * The ring for one assistant line.
56
- *
57
- * A running Turn ticks: every settled tool call advances the stroke one
58
- * segment, and the lap that fills leaves a faint ring behind it — up to
59
- * {@link ACTIVITY_RING_MAX_LAPS_V1}, after which the ring simply loops. A Turn
60
- * that has settled draws no ring; the reply is the answer by then, and the
61
- * component fades the ring out on its way off screen.
62
- */
63
- export function activityRingV1(input: {
64
- toolStatuses: readonly ActivityStepStatusV1[];
65
- status: ActivityTurnStatusV1;
66
- }): ActivityRingViewV1 {
67
- const running = input.status === "streaming";
68
- const steps = input.toolStatuses.filter(
69
- (status) => status !== "running",
70
- ).length;
71
- if (!running) {
72
- return {
73
- active: false,
74
- running: false,
75
- steps,
76
- filled: 0,
77
- laps: 0,
78
- progress: 0,
79
- };
80
- }
81
- const filled = steps % ACTIVITY_RING_SEGMENTS_V1;
82
- const laps = Math.min(
83
- Math.floor(steps / ACTIVITY_RING_SEGMENTS_V1),
84
- ACTIVITY_RING_MAX_LAPS_V1,
85
- );
86
- return {
87
- active: true,
88
- running: true,
89
- steps,
90
- filled,
91
- laps,
92
- progress: filled / ACTIVITY_RING_SEGMENTS_V1,
93
- };
94
- }