@frockbot/plugin-shell 0.3.9 → 0.3.11

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.
@@ -2,6 +2,7 @@
2
2
  import { clientSurfaceRegistryKey } from "@frockbot/client-core";
3
3
  import {
4
4
  announceUiAnchor,
5
+ UiActivityRing,
5
6
  UiIcon,
6
7
  UiIconButton,
7
8
  UiMarkdown,
@@ -26,6 +27,7 @@ import {
26
27
  type WebToolActivity,
27
28
  } from "../shared.js";
28
29
  import { ComposerDraftStore } from "./composer-draft.js";
30
+ import { activityRingV1 } from "./activity-ring.js";
29
31
  import {
30
32
  TURN_TEXT_MAX_CHARACTERS_V1,
31
33
  turnTextCounterVisibleV1,
@@ -431,66 +433,22 @@ function taskChipsOf(message: WebChatMessage): Array<{
431
433
  }
432
434
 
433
435
  /**
434
- * The tools this Turn ran, as the thread draws them.
436
+ * The activity ring for one assistant line.
435
437
  *
436
- * A tool whose Package draws its own surface is shown by that surface; every
437
- * other one is a chip, because a Turn that spends a minute making tool calls
438
- * used to show the User nothing at all but a spinning avatar.
438
+ * A Turn that spends a minute making tool calls used to show the User nothing
439
+ * but a breathing avatar, and then briefly a list of tool names, which put
440
+ * the model's plumbing into a conversation. The ring is neither: it pulses
441
+ * while the Turn runs and ticks forward for every step that settles, so the
442
+ * account of an ordinary tool call is a segment of a stroke and no words at
443
+ * all. The rule lives in `activity-ring.ts`; this only reads the message.
439
444
  */
440
- function toolChipsOf(message: WebChatMessage): WebToolActivity[] {
441
- return message.tools.filter((tool) => iframeEntriesFor(tool).length === 0);
442
- }
443
-
444
- /**
445
- * What a chip calls a tool, in the User's words rather than the model's.
446
- *
447
- * A tool name is an identifier — `send_to_user`, `user-Github--acme/search_issues`
448
- * — and the transcript is a conversation, so the chip drops the namespace,
449
- * un-snakes the rest and capitalises it.
450
- */
451
- function toolChipLabel(tool: WebToolActivity): string {
452
- const bare = tool.name.split("/").pop() ?? tool.name;
453
- const words = bare.replace(/[_.-]+/g, " ").trim();
454
- if (words.length === 0) return tool.name;
455
- return words.charAt(0).toUpperCase() + words.slice(1);
456
- }
457
-
458
- /**
459
- * Whether a chip is drawn as a failure.
460
- *
461
- * A tool call the model recovered from is not a failure the User has anything
462
- * to do with: a refused call followed by a Turn that went on to finish is the
463
- * Bot correcting itself, and colouring it red reports a broken Turn that
464
- * worked. Only a Turn that itself ended badly keeps the failed state.
465
- */
466
- function toolChipState(
467
- tool: WebToolActivity,
445
+ function activityRingOf(
468
446
  message: WebChatMessage,
469
- ): "running" | "completed" | "failed" | "retried" {
470
- if (tool.status !== "failed") return tool.status;
471
- return message.status === "completed" || message.status === "streaming"
472
- ? "retried"
473
- : "failed";
474
- }
475
-
476
- /** What a chip says a tool is doing. Its status, in the User's words. */
477
- function toolChipStatus(
478
- tool: WebToolActivity,
479
- message: WebChatMessage,
480
- ): string {
481
- const state = toolChipState(tool, message);
482
- if (state === "running") return "running";
483
- if (state === "retried") return "retried";
484
- return state === "failed" ? "failed" : "done";
485
- }
486
-
487
- /** Which tool chips the User has opened. Local, and per chip. */
488
- const expandedTools = ref(new Set<string>());
489
-
490
- function toggleTool(toolId: string): void {
491
- const next = new Set(expandedTools.value);
492
- if (!next.delete(toolId)) next.add(toolId);
493
- expandedTools.value = next;
447
+ ): ReturnType<typeof activityRingV1> {
448
+ return activityRingV1({
449
+ toolStatuses: message.tools.map((tool) => tool.status),
450
+ status: message.status,
451
+ });
494
452
  }
495
453
 
496
454
  /** Which chips the User has opened. Local, and per chip. */
@@ -928,28 +886,45 @@ function handleComposerKeydown(event: KeyboardEvent): void {
928
886
  </p>
929
887
  <template v-else-if="message.role === 'assistant'">
930
888
  <!--
931
- The Bot's own avatar comes from whichever Package owns Bot
932
- identity. When no Package fills the slot the sparkle tile is
933
- the only child and shows through.
889
+ The Bot's own avatar, which appears only while it is working.
890
+ Every line in this transcript is from the same Bot — there are
891
+ no group conversations yet (issue 152) — so a sheep beside a
892
+ settled reply named nobody the reader did not already know. The
893
+ one beside a running Turn carries the ring, which is the whole
894
+ point of drawing it.
895
+
896
+ The art comes from whichever Package owns Bot identity; when no
897
+ Package fills the slot the sparkle tile is the only child and
898
+ shows through.
934
899
  -->
935
- <div
936
- class="bot-avatar"
937
- :class="{
938
- 'bot-avatar-live': message.status === 'streaming',
939
- 'bot-avatar-waiting':
940
- message.status === 'streaming' && !message.text,
941
- }"
942
- >
943
- <span class="bot-avatar-fallback" aria-hidden="true"
944
- ><UiIcon name="sparkle" size="sm"
945
- /></span>
946
- <k-slot name="frockbot.bot-avatar" />
947
- </div>
900
+ <Transition name="activity-ring">
901
+ <div
902
+ v-if="activityRingOf(message).active"
903
+ class="bot-avatar bot-avatar-live"
904
+ :class="{ 'bot-avatar-waiting': !message.text }"
905
+ >
906
+ <span class="bot-avatar-fallback" aria-hidden="true"
907
+ ><UiIcon name="sparkle" size="sm"
908
+ /></span>
909
+ <k-slot name="frockbot.bot-avatar" />
910
+ <!--
911
+ What the Bot is doing, while it is doing it — a stroke round
912
+ the sheep that pulses and ticks a segment for every step the
913
+ Turn settles. It completes and fades with the avatar when
914
+ the Turn settles, and it never names a tool: the transcript
915
+ stays a conversation.
916
+ -->
917
+ <UiActivityRing
918
+ :progress="activityRingOf(message).progress"
919
+ :running="activityRingOf(message).running"
920
+ :laps="activityRingOf(message).laps"
921
+ />
922
+ </div>
923
+ </Transition>
948
924
  <!--
949
- Everything the Turn produced stacks in one column beside the
950
- avatar. The row holds exactly two children avatar, column
951
- so a bubble, a notice and a chip are stacked lines rather than
952
- side-by-side columns squeezing the reply to a few pixels.
925
+ Everything the Turn produced stacks in one column. While the Bot
926
+ is working the avatar is beside it; once the Turn settles the
927
+ column is the whole row and starts at the transcript's edge.
953
928
  -->
954
929
  <div class="message-column">
955
930
  <div v-if="message.text" class="message-bubble">
@@ -1024,40 +999,6 @@ function handleComposerKeydown(event: KeyboardEvent): void {
1024
999
  />
1025
1000
  </div>
1026
1001
  <!--
1027
- What the Bot did, while it is doing it. The chip is the
1028
- conversation's whole account of an ordinary tool call: its
1029
- name, whether it is running, and — when the User opens it —
1030
- what it returned.
1031
- -->
1032
- <div
1033
- v-if="toolChipsOf(message).length > 0"
1034
- class="message-tools"
1035
- >
1036
- <button
1037
- v-for="tool in toolChipsOf(message)"
1038
- :key="tool.id"
1039
- type="button"
1040
- class="tool-chip"
1041
- :class="`tool-chip-${toolChipState(tool, message)}`"
1042
- :aria-expanded="expandedTools.has(tool.id)"
1043
- @click="toggleTool(tool.id)"
1044
- >
1045
- <span class="tool-chip-name">{{
1046
- toolChipLabel(tool)
1047
- }}</span>
1048
- <span class="tool-chip-status">{{
1049
- toolChipStatus(tool, message)
1050
- }}</span>
1051
- <span
1052
- v-if="
1053
- expandedTools.has(tool.id) && tool.text !== undefined
1054
- "
1055
- class="tool-chip-result"
1056
- >{{ tool.text }}</span
1057
- >
1058
- </button>
1059
- </div>
1060
- <!--
1061
1002
  The subagents this Turn dispatched. The child's own Session is
1062
1003
  never in this transcript, so the chip is the whole of what the
1063
1004
  conversation says about it; opening one shows the summary the
@@ -0,0 +1,89 @@
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
+ });
@@ -0,0 +1,94 @@
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
+ }
@@ -1228,6 +1228,7 @@ export const shellClientPlugin: ClientPlugin = (ctx) => {
1228
1228
  packageDisplayName: catalogPackage?.displayName,
1229
1229
  connectionDisplayName: connection?.displayName,
1230
1230
  failure: effective.binding?.failure,
1231
+ fallback: Boolean(effective.fallback),
1231
1232
  });
1232
1233
  }
1233
1234
 
@@ -35,6 +35,20 @@ describe("model runtime presentation", () => {
35
35
  );
36
36
  });
37
37
 
38
+ test("names the stand-in model when the chosen one is unavailable", () => {
39
+ // The Bot is answering, so the line is not a failure — it names the model
40
+ // actually in use and says the User's own choice is not it.
41
+ expect(
42
+ modelRuntimeLabel({
43
+ source: "platform",
44
+ modelDisplayName: "Auto",
45
+ providerModelId: "@flock/auto",
46
+ packageDisplayName: "Flock AI",
47
+ fallback: true,
48
+ }),
49
+ ).toBe("Auto · Flock AI · your chosen model is unavailable");
50
+ });
51
+
38
52
  test("shows unavailable and backend failure states", () => {
39
53
  expect(modelRuntimeLabel({ source: "none" })).toBe(
40
54
  "No model available — set one up in Models",
@@ -11,6 +11,11 @@ export function modelRuntimeLabel(input: {
11
11
  packageDisplayName?: string;
12
12
  connectionDisplayName?: string;
13
13
  failure?: string;
14
+ /**
15
+ * The chosen model could not bind — its provider Package is off, or its
16
+ * Connection is gone — and the platform default is answering in its place.
17
+ */
18
+ fallback?: boolean;
14
19
  }): string {
15
20
  if (input.failure) return input.failure;
16
21
  if (input.source === "none" || !input.providerModelId) {
@@ -22,5 +27,6 @@ export function modelRuntimeLabel(input: {
22
27
  const runtime = provider ? `${model} · ${provider}` : model;
23
28
  if (input.source === "bot") return `${runtime} · this Bot only`;
24
29
  if (input.source === "account") return `${runtime} · Account model`;
30
+ if (input.fallback) return `${runtime} · your chosen model is unavailable`;
25
31
  return runtime;
26
32
  }
@@ -1,4 +1,10 @@
1
1
  .frockbot-root {
2
+ /*
3
+ * The clear margin a bubble leaves on the side it is not anchored to — about
4
+ * a centimetre and a half. It is what says which side of the conversation a
5
+ * line is on, so it is the same on a phone as on a desktop.
6
+ */
7
+ --frock-bubble-gutter: 56px;
2
8
  position: fixed;
3
9
  z-index: 1000;
4
10
  inset: 0;
@@ -324,10 +330,14 @@
324
330
  }
325
331
 
326
332
  /*
327
- * An assistant Turn is its avatar and, beside it, one column holding
328
- * everything the Turn produced. The row has exactly two children: bubbles,
329
- * notices, sends and chips stack inside the column, so a one-word reply is a
330
- * bubble the width of its word rather than a sliver of a shared row.
333
+ * An assistant Turn is one column holding everything it produced: bubbles,
334
+ * notices and sends stack inside it, so a one-word reply is a bubble the width
335
+ * of its word rather than a sliver of a shared row.
336
+ *
337
+ * The avatar is beside that column only while the Bot is working. Every reply
338
+ * in this transcript is from the same Bot — there are no group conversations —
339
+ * so a sheep on every settled line said nothing and cost the column its left
340
+ * edge. The one on the running Turn does say something, which is why it stays.
331
341
  */
332
342
  .message-assistant {
333
343
  flex-direction: row;
@@ -381,20 +391,22 @@
381
391
  animation: frock-breathe 2600ms ease-in-out infinite;
382
392
  }
383
393
 
384
- .bot-avatar-live::after {
385
- position: absolute;
386
- border-radius: 12px;
387
- animation: frock-halo 2600ms ease-in-out infinite;
388
- box-shadow: 0 0 0 2px var(--frock-surface-accent);
389
- content: "";
390
- inset: -4px;
391
- opacity: 0;
392
- pointer-events: none;
394
+ .bot-avatar-waiting {
395
+ animation-duration: 1600ms;
393
396
  }
394
397
 
395
- .bot-avatar-waiting,
396
- .bot-avatar-waiting::after {
397
- animation-duration: 1600ms;
398
+ /*
399
+ * The ring's way off screen. The Turn has settled, the reply is the answer
400
+ * now, and the stroke fades rather than vanishing between two frames.
401
+ */
402
+ .activity-ring-enter-active,
403
+ .activity-ring-leave-active {
404
+ transition: opacity 420ms ease-out;
405
+ }
406
+
407
+ .activity-ring-enter-from,
408
+ .activity-ring-leave-to {
409
+ opacity: 0;
398
410
  }
399
411
 
400
412
  @keyframes frock-breathe {
@@ -408,22 +420,16 @@
408
420
  }
409
421
  }
410
422
 
411
- @keyframes frock-halo {
412
- 0%,
413
- 100% {
414
- opacity: 0;
415
- transform: scale(0.94);
416
- }
417
-
418
- 50% {
419
- opacity: 1;
420
- transform: scale(1.06);
421
- }
422
- }
423
-
423
+ /*
424
+ * A bubble runs nearly the width of the transcript, leaving one clear margin
425
+ * on the side it is not anchored to: the Bot's at the end, the User's at the
426
+ * start. That margin is the only thing distinguishing the two columns now that
427
+ * neither carries an avatar, so it is a fixed gap rather than a percentage —
428
+ * it has to stay legible at every width.
429
+ */
424
430
  .message-bubble {
425
431
  width: max-content;
426
- max-width: min(640px, 84%);
432
+ max-width: calc(100% - var(--frock-bubble-gutter));
427
433
  padding: 10px 14px;
428
434
  border: 1px solid var(--frock-border);
429
435
  border-radius: 18px 18px 18px 6px;
@@ -438,7 +444,7 @@
438
444
  }
439
445
 
440
446
  .message-user .message-bubble {
441
- max-width: min(640px, 84%);
447
+ max-width: calc(100% - var(--frock-bubble-gutter));
442
448
  border-color: var(--frock-action-primary);
443
449
  border-radius: 18px 18px 6px 18px;
444
450
  color: var(--frock-on-accent);
@@ -453,59 +459,9 @@
453
459
 
454
460
  .message-sends {
455
461
  display: flex;
462
+ width: calc(100% - var(--frock-bubble-gutter));
456
463
  flex-direction: column;
457
464
  gap: 8px;
458
- width: min(640px, 84%);
459
- }
460
-
461
- /*
462
- * Tool calls. Quieter than anything the Bot said: the User is watching work
463
- * happen, not reading a message, so a chip carries the name and its state and
464
- * opens in place to what the tool returned.
465
- */
466
-
467
- .message-tools {
468
- display: flex;
469
- flex-direction: column;
470
- gap: 6px;
471
- width: min(640px, 84%);
472
- }
473
-
474
- .tool-chip {
475
- display: flex;
476
- flex-wrap: wrap;
477
- gap: 8px;
478
- align-items: baseline;
479
- padding: 6px 10px;
480
- font: inherit;
481
- color: var(--frock-text-muted);
482
- text-align: left;
483
- cursor: pointer;
484
- background: var(--frock-surface);
485
- border: 1px solid var(--frock-border);
486
- border-radius: 10px;
487
- }
488
-
489
- .tool-chip-name {
490
- color: var(--frock-text);
491
- font-weight: 600;
492
- }
493
-
494
- .tool-chip-status {
495
- flex: 1 1 auto;
496
- font-size: var(--frock-text-xs);
497
- }
498
-
499
- .tool-chip-failed .tool-chip-status {
500
- color: var(--frock-danger-text);
501
- }
502
-
503
- .tool-chip-result {
504
- flex: 1 0 100%;
505
- max-height: 12em;
506
- overflow: auto;
507
- white-space: pre-wrap;
508
- word-break: break-word;
509
465
  }
510
466
 
511
467
  /*
@@ -516,9 +472,9 @@
516
472
 
517
473
  .message-tasks {
518
474
  display: flex;
475
+ width: calc(100% - var(--frock-bubble-gutter));
519
476
  flex-direction: column;
520
477
  gap: 6px;
521
- width: min(640px, 84%);
522
478
  }
523
479
 
524
480
  .task-chip {
@@ -1169,16 +1125,6 @@
1169
1125
  padding: 20px;
1170
1126
  }
1171
1127
 
1172
- .message-bubble,
1173
- .message-user .message-bubble {
1174
- max-width: 92%;
1175
- }
1176
-
1177
- .message-sends,
1178
- .message-tasks {
1179
- width: 92%;
1180
- }
1181
-
1182
1128
  .message-attachments img {
1183
1129
  max-width: 100%;
1184
1130
  }