@frockbot/plugin-shell 0.3.5 → 0.3.7

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.
@@ -288,7 +288,7 @@ const threadHeading = computed(() => {
288
288
  const threadHint = computed(() => {
289
289
  if (!hasBot.value) return "Add your first sheep to start a conversation.";
290
290
  if (state.value.modelReady) {
291
- return "Start with a conversation. Cordis plugins can add the rest.";
291
+ return "Say anything to get started.";
292
292
  }
293
293
  return state.value.modelLabel;
294
294
  });
@@ -416,10 +416,47 @@ function toolChipsOf(message: WebChatMessage): WebToolActivity[] {
416
416
  return message.tools.filter((tool) => iframeEntriesFor(tool).length === 0);
417
417
  }
418
418
 
419
+ /**
420
+ * What a chip calls a tool, in the User's words rather than the model's.
421
+ *
422
+ * A tool name is an identifier — `send_to_user`, `user-Github--acme/search_issues`
423
+ * — and the transcript is a conversation, so the chip drops the namespace,
424
+ * un-snakes the rest and capitalises it.
425
+ */
426
+ function toolChipLabel(tool: WebToolActivity): string {
427
+ const bare = tool.name.split("/").pop() ?? tool.name;
428
+ const words = bare.replace(/[_.-]+/g, " ").trim();
429
+ if (words.length === 0) return tool.name;
430
+ return words.charAt(0).toUpperCase() + words.slice(1);
431
+ }
432
+
433
+ /**
434
+ * Whether a chip is drawn as a failure.
435
+ *
436
+ * A tool call the model recovered from is not a failure the User has anything
437
+ * to do with: a refused call followed by a Turn that went on to finish is the
438
+ * Bot correcting itself, and colouring it red reports a broken Turn that
439
+ * worked. Only a Turn that itself ended badly keeps the failed state.
440
+ */
441
+ function toolChipState(
442
+ tool: WebToolActivity,
443
+ message: WebChatMessage,
444
+ ): "running" | "completed" | "failed" | "retried" {
445
+ if (tool.status !== "failed") return tool.status;
446
+ return message.status === "completed" || message.status === "streaming"
447
+ ? "retried"
448
+ : "failed";
449
+ }
450
+
419
451
  /** What a chip says a tool is doing. Its status, in the User's words. */
420
- function toolChipStatus(tool: WebToolActivity): string {
421
- if (tool.status === "running") return "running";
422
- return tool.status === "failed" ? "failed" : "done";
452
+ function toolChipStatus(
453
+ tool: WebToolActivity,
454
+ message: WebChatMessage,
455
+ ): string {
456
+ const state = toolChipState(tool, message);
457
+ if (state === "running") return "running";
458
+ if (state === "retried") return "retried";
459
+ return state === "failed" ? "failed" : "done";
423
460
  }
424
461
 
425
462
  /** Which tool chips the User has opened. Local, and per chip. */
@@ -883,139 +920,159 @@ function handleComposerKeydown(event: KeyboardEvent): void {
883
920
  /></span>
884
921
  <k-slot name="frockbot.bot-avatar" />
885
922
  </div>
886
- <div v-if="message.text" class="message-bubble">
887
- <UiMarkdown :text="message.text" />
888
- </div>
889
923
  <!--
924
+ Everything the Turn produced stacks in one column beside the
925
+ avatar. The row holds exactly two children — avatar, column —
926
+ so a bubble, a notice and a chip are stacked lines rather than
927
+ side-by-side columns squeezing the reply to a few pixels.
928
+ -->
929
+ <div class="message-column">
930
+ <div v-if="message.text" class="message-bubble">
931
+ <UiMarkdown :text="message.text" />
932
+ </div>
933
+ <!--
890
934
  Why the Turn ends where it does, under whatever it had already
891
935
  said rather than in place of it.
892
936
  -->
893
- <p v-if="message.notice" class="message-notice">
894
- {{ message.notice }}
895
- </p>
896
- <template v-for="tool in message.tools" :key="tool.id">
897
- <PackageIframeHost
898
- v-for="entry in iframeEntriesFor(tool)"
899
- :key="`${tool.id}:${entry.contribution.packageId}:${entry.page.id}`"
900
- class="message-package-iframe"
901
- :contribution="entry.contribution"
902
- :page="entry.page"
903
- :slot="entry.slot"
904
- :states="{ [`tool:${tool.name}`]: toolResultState(tool) }"
905
- />
906
- </template>
907
- <!--
937
+ <p v-if="message.notice" class="message-notice">
938
+ {{ message.notice }}
939
+ </p>
940
+ <template v-for="tool in message.tools" :key="tool.id">
941
+ <PackageIframeHost
942
+ v-for="entry in iframeEntriesFor(tool)"
943
+ :key="`${tool.id}:${entry.contribution.packageId}:${entry.page.id}`"
944
+ class="message-package-iframe"
945
+ :contribution="entry.contribution"
946
+ :page="entry.page"
947
+ :slot="entry.slot"
948
+ :states="{ [`tool:${tool.name}`]: toolResultState(tool) }"
949
+ />
950
+ </template>
951
+ <!--
908
952
  A binary a tool filed in the Workspace, drawn from the
909
953
  Workspace read route. The thread carries the path, never the
910
954
  bytes, so a long conversation costs paths and the image is
911
955
  fetched only when it is on screen.
912
956
  -->
913
- <div
914
- v-if="attachmentsOf(message).length > 0"
915
- class="message-attachments"
916
- >
917
- <a
918
- v-for="attachment in attachmentsOf(message)"
919
- :key="attachment.contentHash"
920
- :href="workspaceFileUrl(attachment.path)"
921
- target="_blank"
922
- rel="noreferrer"
957
+ <div
958
+ v-if="attachmentsOf(message).length > 0"
959
+ class="message-attachments"
923
960
  >
924
- <img
925
- :src="workspaceFileUrl(attachment.path)"
926
- :alt="`Attachment from ${message.runId}`"
927
- loading="lazy"
928
- />
929
- </a>
930
- </div>
931
- <!--
961
+ <a
962
+ v-for="attachment in attachmentsOf(message)"
963
+ :key="attachment.contentHash"
964
+ :href="workspaceFileUrl(attachment.path)"
965
+ target="_blank"
966
+ rel="noreferrer"
967
+ >
968
+ <img
969
+ :src="workspaceFileUrl(attachment.path)"
970
+ :alt="`Attachment from ${message.runId}`"
971
+ loading="lazy"
972
+ />
973
+ </a>
974
+ </div>
975
+ <!--
932
976
  Sends sit beside the derived text rather than inside it: each
933
977
  payload is its own block, and a widget-ended Turn has no text
934
978
  bubble at all.
935
979
  -->
936
- <div v-if="message.sends.length > 0" class="message-sends">
937
- <SendPayloadView
938
- v-for="(send, sendIndex) in message.sends"
939
- :key="sendIndex"
940
- :send="send"
941
- />
942
- </div>
943
- <!--
980
+ <div v-if="message.sends.length > 0" class="message-sends">
981
+ <SendPayloadView
982
+ v-for="(send, sendIndex) in message.sends"
983
+ :key="sendIndex"
984
+ :send="send"
985
+ />
986
+ </div>
987
+ <!--
944
988
  What the Bot did, while it is doing it. The chip is the
945
989
  conversation's whole account of an ordinary tool call: its
946
990
  name, whether it is running, and — when the User opens it —
947
991
  what it returned.
948
992
  -->
949
- <div v-if="toolChipsOf(message).length > 0" class="message-tools">
950
- <button
951
- v-for="tool in toolChipsOf(message)"
952
- :key="tool.id"
953
- type="button"
954
- class="tool-chip"
955
- :class="`tool-chip-${tool.status}`"
956
- :aria-expanded="expandedTools.has(tool.id)"
957
- @click="toggleTool(tool.id)"
993
+ <div
994
+ v-if="toolChipsOf(message).length > 0"
995
+ class="message-tools"
958
996
  >
959
- <span class="tool-chip-name">{{ tool.name }}</span>
960
- <span class="tool-chip-status">{{
961
- toolChipStatus(tool)
962
- }}</span>
963
- <span
964
- v-if="expandedTools.has(tool.id) && tool.text !== undefined"
965
- class="tool-chip-result"
966
- >{{ tool.text }}</span
997
+ <button
998
+ v-for="tool in toolChipsOf(message)"
999
+ :key="tool.id"
1000
+ type="button"
1001
+ class="tool-chip"
1002
+ :class="`tool-chip-${toolChipState(tool, message)}`"
1003
+ :aria-expanded="expandedTools.has(tool.id)"
1004
+ @click="toggleTool(tool.id)"
967
1005
  >
968
- </button>
969
- </div>
970
- <!--
1006
+ <span class="tool-chip-name">{{
1007
+ toolChipLabel(tool)
1008
+ }}</span>
1009
+ <span class="tool-chip-status">{{
1010
+ toolChipStatus(tool, message)
1011
+ }}</span>
1012
+ <span
1013
+ v-if="
1014
+ expandedTools.has(tool.id) && tool.text !== undefined
1015
+ "
1016
+ class="tool-chip-result"
1017
+ >{{ tool.text }}</span
1018
+ >
1019
+ </button>
1020
+ </div>
1021
+ <!--
971
1022
  The subagents this Turn dispatched. The child's own Session is
972
1023
  never in this transcript, so the chip is the whole of what the
973
1024
  conversation says about it; opening one shows the summary the
974
1025
  child handed back and nothing else.
975
1026
  -->
976
- <div v-if="taskChipsOf(message).length > 0" class="message-tasks">
977
- <button
978
- v-for="entry in taskChipsOf(message)"
979
- :key="entry.chip.taskId"
980
- type="button"
981
- class="task-chip"
982
- :class="`task-chip-${entry.status}`"
983
- :aria-expanded="expandedTasks.has(entry.chip.taskId)"
984
- @click="toggleTask(entry.chip.taskId)"
1027
+ <div
1028
+ v-if="taskChipsOf(message).length > 0"
1029
+ class="message-tasks"
985
1030
  >
986
- <span class="task-chip-type">{{ entry.chip.taskType }}</span>
987
- <span class="task-chip-description">{{
988
- entry.chip.description
989
- }}</span>
990
- <span class="task-chip-status">{{ entry.status }}</span>
991
- <span class="task-chip-model">{{ entry.chip.model }}</span>
992
- <span
993
- v-if="expandedTasks.has(entry.chip.taskId)"
994
- class="task-chip-summary"
995
- >{{
996
- entry.summary ??
997
- "This subagent has not reported a summary yet."
998
- }}</span
1031
+ <button
1032
+ v-for="entry in taskChipsOf(message)"
1033
+ :key="entry.chip.taskId"
1034
+ type="button"
1035
+ class="task-chip"
1036
+ :class="`task-chip-${entry.status}`"
1037
+ :aria-expanded="expandedTasks.has(entry.chip.taskId)"
1038
+ @click="toggleTask(entry.chip.taskId)"
999
1039
  >
1000
- <!--
1040
+ <span class="task-chip-type">{{
1041
+ entry.chip.taskType
1042
+ }}</span>
1043
+ <span class="task-chip-description">{{
1044
+ entry.chip.description
1045
+ }}</span>
1046
+ <span class="task-chip-status">{{ entry.status }}</span>
1047
+ <span class="task-chip-model">{{ entry.chip.model }}</span>
1048
+ <span
1049
+ v-if="expandedTasks.has(entry.chip.taskId)"
1050
+ class="task-chip-summary"
1051
+ >{{
1052
+ entry.summary ??
1053
+ "This subagent has not reported a summary yet."
1054
+ }}</span
1055
+ >
1056
+ <!--
1001
1057
  Cancellation is the User's, explicit and authenticated. It
1002
1058
  is offered only while the subagent is live: a settled one
1003
1059
  has an outcome, and stopping it would rewrite it.
1004
1060
  -->
1005
- <span
1006
- v-if="
1007
- expandedTasks.has(entry.chip.taskId) &&
1008
- isTaskLive(entry.status)
1009
- "
1010
- class="task-chip-stop"
1011
- role="button"
1012
- tabindex="0"
1013
- @click.stop="stopTask(entry.chip.taskId)"
1014
- @keydown.enter.stop.prevent="stopTask(entry.chip.taskId)"
1015
- @keydown.space.stop.prevent="stopTask(entry.chip.taskId)"
1016
- >Stop this subagent</span
1017
- >
1018
- </button>
1061
+ <span
1062
+ v-if="
1063
+ expandedTasks.has(entry.chip.taskId) &&
1064
+ isTaskLive(entry.status)
1065
+ "
1066
+ class="task-chip-stop"
1067
+ role="button"
1068
+ tabindex="0"
1069
+ @click.stop="stopTask(entry.chip.taskId)"
1070
+ @keydown.enter.stop.prevent="stopTask(entry.chip.taskId)"
1071
+ @keydown.space.stop.prevent="stopTask(entry.chip.taskId)"
1072
+ >Stop this subagent</span
1073
+ >
1074
+ </button>
1075
+ </div>
1019
1076
  </div>
1020
1077
  </template>
1021
1078
  <div v-else class="message-bubble">{{ message.text }}</div>
@@ -1046,7 +1103,7 @@ function handleComposerKeydown(event: KeyboardEvent): void {
1046
1103
  type="button"
1047
1104
  @click="web.resumeRun(state.activeRun.runId)"
1048
1105
  >
1049
- Resolve Turn
1106
+ Try again
1050
1107
  </button>
1051
1108
  </div>
1052
1109
  </Transition>
@@ -1132,6 +1189,19 @@ function handleComposerKeydown(event: KeyboardEvent): void {
1132
1189
  @blur="closeSkillPopover"
1133
1190
  />
1134
1191
  </div>
1192
+ <!--
1193
+ Start a new conversation. Sits beside the composer because that is
1194
+ where you are when you decide the last one is finished. Disabled
1195
+ while a Turn is running: the Bot is still writing to it.
1196
+ -->
1197
+ <UiIconButton
1198
+ icon="plus"
1199
+ label="New conversation"
1200
+ variant="ghost"
1201
+ class="new-conversation-button"
1202
+ :disabled="isRunning"
1203
+ @click="web.startConversation()"
1204
+ />
1135
1205
  <UiIconButton
1136
1206
  v-if="showStop"
1137
1207
  class="stop-button"
@@ -34,6 +34,13 @@ const providedWeb = inject(frockBotWebDataKey);
34
34
  if (!providedWeb) throw new Error("Package iframe host data was not provided");
35
35
  const web = providedWeb;
36
36
  const frame = ref<HTMLIFrameElement>();
37
+ /** Where a page came from, said plainly. First-party pages say nothing. */
38
+ const provenanceLabel = computed(() => {
39
+ if (props.contribution.provenance === "Bot-authored")
40
+ return "Built by this Bot";
41
+ if (props.contribution.provenance === "User-installed") return "Added by you";
42
+ return undefined;
43
+ });
37
44
  const height = ref(240);
38
45
  const failure = ref<string>();
39
46
  const lastStateWireByName = new Map<string, string>();
@@ -123,7 +130,9 @@ function post(message: PackageIframeHostMessageV2): void {
123
130
  postPackageIframeHostMessage(target, message, lastStateWireByName);
124
131
  } catch (error) {
125
132
  failure.value =
126
- error instanceof Error ? error.message : "Package page state is invalid";
133
+ error instanceof Error
134
+ ? error.message
135
+ : "This page sent something FrockBot couldn't read.";
127
136
  }
128
137
  }
129
138
 
@@ -179,7 +188,7 @@ async function onMessage(event: MessageEvent): Promise<void> {
179
188
  }
180
189
  if (message.type === "focus") {
181
190
  if (!packageIframeFocusAllowedV2(props.contribution)) {
182
- failure.value = "This Package cannot change the focused Applet.";
191
+ failure.value = "This plugin can't change which Applet is open.";
183
192
  return;
184
193
  }
185
194
  failure.value = undefined;
@@ -189,7 +198,7 @@ async function onMessage(event: MessageEvent): Promise<void> {
189
198
  if (message.type === "openExternal") {
190
199
  const origin = catalog.value?.artifactOrigin;
191
200
  if (!origin || !packageIframeExternalUrlAllowedV2(message.url, origin)) {
192
- failure.value = "This Package page can only open its own pages.";
201
+ failure.value = "This plugin can only open its own pages.";
193
202
  return;
194
203
  }
195
204
  failure.value = undefined;
@@ -197,7 +206,7 @@ async function onMessage(event: MessageEvent): Promise<void> {
197
206
  return;
198
207
  }
199
208
  if (!packageIframeToolAllowedV1(props.contribution, message.name)) {
200
- failure.value = `This Package did not declare ${message.name}.`;
209
+ failure.value = `This plugin isn't allowed to use ${message.name}.`;
201
210
  return;
202
211
  }
203
212
  failure.value = undefined;
@@ -232,12 +241,12 @@ onBeforeUnmount(() => window.removeEventListener("message", onMessage));
232
241
  <section class="package-iframe-frame" :class="`package-iframe-${layout}`">
233
242
  <header v-if="attribution" class="package-iframe-attribution">
234
243
  <strong>{{ contribution.displayName }}</strong>
235
- <span>{{ contribution.provenance }} Package</span>
244
+ <span v-if="provenanceLabel">{{ provenanceLabel }}</span>
236
245
  </header>
237
246
  <!-- Load eagerly because lazy iframes defer the init/resize handshake until the browser decides the frame is near the viewport, which headless Chromium may never do. -->
238
247
  <iframe
239
248
  ref="frame"
240
- :title="`${contribution.displayName} Package page`"
249
+ :title="contribution.displayName"
241
250
  :src="source"
242
251
  :style="layout === 'fill' ? undefined : { height: `${height}px` }"
243
252
  sandbox="allow-scripts"
@@ -516,7 +516,7 @@ describe("Bot selection", () => {
516
516
  const requestCount = requests.length;
517
517
  await expect(
518
518
  provided.value.callPackageUiTool(contribution, "package_author", {}),
519
- ).rejects.toThrow("did not declare");
519
+ ).rejects.toThrow("isn't allowed to use");
520
520
  expect(requests).toHaveLength(requestCount);
521
521
  });
522
522
 
@@ -571,6 +571,47 @@ describe("Bot selection", () => {
571
571
  expect(provided.value.settingsError).toBe("Bot settings unavailable");
572
572
  });
573
573
 
574
+ test("says a deployment has no Catalog instead of relaying the gateway's sentence", async () => {
575
+ // F4: the Plugins/Catalog surface rendered `catalog generation was not
576
+ // found` beside "No Catalog entry matches that search", for a User who had
577
+ // searched nothing on a deployment that had published nothing.
578
+ let provided: Ref<FrockBotWebData> | undefined;
579
+ let failure = new Error("catalog generation was not found");
580
+ await shellClientPlugin({
581
+ transport: {
582
+ turn: () => Promise.resolve({ runId: "run", text: "", events: [] }),
583
+ readConfiguration: () => Promise.reject(new Error("unused")),
584
+ hostedRequest: (path) => {
585
+ expect(path).toBe("/catalog/v1/index");
586
+ return Promise.reject(failure);
587
+ },
588
+ },
589
+ slot: () => () => {},
590
+ inject: () => {
591
+ throw new Error("unexpected client provider injection");
592
+ },
593
+ provide: (_key, value) => {
594
+ provided = value as Ref<FrockBotWebData>;
595
+ return () => {};
596
+ },
597
+ });
598
+ if (!provided) throw new Error("shell data was not provided");
599
+
600
+ await provided.value.loadPackageCatalog();
601
+ expect(provided.value.packageCatalog).toEqual([]);
602
+ expect(provided.value.packageCatalogGeneration).toBeUndefined();
603
+ expect(provided.value.settingsError).toBe(
604
+ "No plugins are published for this deployment yet.",
605
+ );
606
+
607
+ // A genuine fault still reaches the operator, labelled as one.
608
+ failure = new Error("R2 is down");
609
+ await provided.value.loadPackageCatalog();
610
+ expect(provided.value.settingsError).toBe(
611
+ "Plugins could not be loaded: R2 is down",
612
+ );
613
+ });
614
+
574
615
  test("commits a catalog without overwriting newer User settings", async () => {
575
616
  let provided: Ref<FrockBotWebData> | undefined;
576
617
  const catalogManifest = Promise.withResolvers<unknown>();
@@ -648,7 +689,7 @@ describe("Bot selection", () => {
648
689
  },
649
690
  accountValues: undefined,
650
691
  platformModel: undefined,
651
- expectedLabel: "Model name · Model provider · Bot override",
692
+ expectedLabel: "Model name · Model provider · this Bot only",
652
693
  expectedProjection: "bot",
653
694
  ready: true,
654
695
  },
@@ -683,7 +724,7 @@ describe("Bot selection", () => {
683
724
  botValues: {},
684
725
  accountValues: undefined,
685
726
  platformModel: undefined,
686
- expectedLabel: "Model unavailable",
727
+ expectedLabel: "No model available — set one up in Models",
687
728
  expectedProjection: "none",
688
729
  ready: false,
689
730
  },
@@ -900,7 +941,7 @@ describe("Bot selection", () => {
900
941
 
901
942
  expect(provided.value.modelReady).toBe(false);
902
943
  expect(provided.value.modelLabel).toBe(
903
- 'Connection "model-connection" is disabled; enable or reconnect it',
944
+ "That account needs reconnecting before this Bot can reply.",
904
945
  );
905
946
  });
906
947
 
@@ -1217,7 +1258,7 @@ describe("detached Turn projection", () => {
1217
1258
  { role: "user", status: "completed" },
1218
1259
  {
1219
1260
  role: "assistant",
1220
- text: "Provider reconciliation is required",
1261
+ text: "This Bot couldn't finish its reply. Try again.",
1221
1262
  status: "error",
1222
1263
  },
1223
1264
  ]);
@@ -1482,6 +1523,55 @@ describe("active durable Turn projection", () => {
1482
1523
  ]);
1483
1524
  });
1484
1525
 
1526
+ test("a Turn that sent something draws the send, not the model's own text", () => {
1527
+ const state: Pick<
1528
+ FrockBotWebData,
1529
+ "messages" | "activeRunId" | "activeRun"
1530
+ > = { messages: [] };
1531
+
1532
+ projectDurableRuns(
1533
+ state,
1534
+ [],
1535
+ [
1536
+ {
1537
+ runId: "run-once",
1538
+ input: "ping",
1539
+ status: "completed",
1540
+ responseText: "pong",
1541
+ events: [
1542
+ { type: "send/to-user", payload: { type: "text", text: "pong" } },
1543
+ ],
1544
+ },
1545
+ ],
1546
+ );
1547
+
1548
+ expect(state.messages[1]?.text).toBe("");
1549
+ expect(state.messages[1]?.sends).toHaveLength(1);
1550
+ });
1551
+
1552
+ test("a Turn that sent nothing still draws the model's text", () => {
1553
+ const state: Pick<
1554
+ FrockBotWebData,
1555
+ "messages" | "activeRunId" | "activeRun"
1556
+ > = { messages: [] };
1557
+
1558
+ projectDurableRuns(
1559
+ state,
1560
+ [],
1561
+ [
1562
+ {
1563
+ runId: "run-plain",
1564
+ input: "ping",
1565
+ status: "completed",
1566
+ responseText: "pong",
1567
+ events: [],
1568
+ },
1569
+ ],
1570
+ );
1571
+
1572
+ expect(state.messages[1]?.text).toBe("pong");
1573
+ });
1574
+
1485
1575
  test("streams running text without a banner and clears busy state", () => {
1486
1576
  const state: Pick<
1487
1577
  FrockBotWebData,
@@ -1562,11 +1652,11 @@ describe("active durable Turn projection", () => {
1562
1652
  expect(reconciliation.activeRun).toEqual({
1563
1653
  runId: "run-reconciliation",
1564
1654
  status: "reconciliation-required",
1565
- message: "Provider result needs confirmation",
1655
+ message: "Something went wrong mid-reply. Try again to pick it up.",
1566
1656
  canResume: true,
1567
1657
  });
1568
1658
  expect(reconciliation.messages[1]).toMatchObject({
1569
- text: "Provider result needs confirmation",
1659
+ text: "This reply stopped partway. Try again to continue it.",
1570
1660
  status: "reconciliation-required",
1571
1661
  });
1572
1662
  });
@@ -1845,8 +1935,7 @@ describe("hosted Stop", () => {
1845
1935
  await provided.value.stopRun();
1846
1936
  expect(provided.value.activeRun).toMatchObject({
1847
1937
  status: "reconciliation-required",
1848
- message:
1849
- "Stop accepted; reconciling the provider outcome before cancelling.",
1938
+ message: "Stopping…",
1850
1939
  // A parked Turn is offered the resolve control, Stop included: hiding
1851
1940
  // it there hid it in exactly the case Stop creates.
1852
1941
  canResume: true,
@@ -1858,7 +1947,7 @@ describe("hosted Stop", () => {
1858
1947
  // The Turn keeps whatever it had already said; the notice is the line
1859
1948
  // that says why it ends where it does.
1860
1949
  expect(provided.value.messages[1]).toMatchObject({
1861
- notice: "Stopped by an authenticated Stop command.",
1950
+ notice: "You stopped this.",
1862
1951
  status: "aborted",
1863
1952
  });
1864
1953
 
@@ -1923,7 +2012,7 @@ describe("hosted Stop", () => {
1923
2012
  expect(provided.value.activeRunId).toBeUndefined();
1924
2013
  expect(provided.value.messages.at(-1)).toMatchObject({
1925
2014
  runId: "run-1",
1926
- notice: "Stopped by an authenticated Stop command.",
2015
+ notice: "You stopped this.",
1927
2016
  status: "aborted",
1928
2017
  });
1929
2018
  });
@@ -2513,7 +2602,7 @@ describe("Connection operation reconciliation", () => {
2513
2602
  ).rejects.toThrow("Connection state update failed");
2514
2603
  await expect(
2515
2604
  provided.value.disconnectConnection("connection-1"),
2516
- ).rejects.toThrow("Connection revocation failed");
2605
+ ).rejects.toThrow("Couldn't disconnect that account.");
2517
2606
  expect(commands).toEqual([
2518
2607
  "connection/update-label",
2519
2608
  "connection/set-enabled",