clay-server 2.12.0 → 2.13.0-beta.1

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/lib/public/app.js CHANGED
@@ -2,6 +2,8 @@ import { showToast, copyToClipboard, escapeHtml } from './modules/utils.js';
2
2
  import { refreshIcons, iconHtml, randomThinkingVerb } from './modules/icons.js';
3
3
  import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks, closeMermaidModal, parseEmojis } from './modules/markdown.js';
4
4
  import { initSidebar, renderSessionList, handleSearchResults, handleSearchContentResults, updateSessionPresence, updatePageTitle, getActiveSearchQuery, buildSearchTimeline, removeSearchTimeline, onHistoryPrepended, populateCliSessionList, renderIconStrip, renderSidebarPresence, initIconStrip, getEmojiCategories, renderUserStrip, setCurrentDmUser, updateDmBadge, updateSessionBadge, updateProjectBadge, closeDmUserPicker, spawnDustParticles } from './modules/sidebar.js';
5
+ import { initMateSidebar, showMateSidebar, hideMateSidebar, renderMateSessionList, updateMateSidebarProfile } from './modules/mate-sidebar.js';
6
+ import { initMateKnowledge, requestKnowledgeList, renderKnowledgeList, handleKnowledgeContent, hideKnowledge } from './modules/mate-knowledge.js';
5
7
  import { initRewind, setRewindMode, showRewindModal, clearPendingRewindUuid, addRewindButton } from './modules/rewind.js';
6
8
  import { initNotifications, showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './modules/notifications.js';
7
9
  import { initInput, clearPendingImages, handleInputSync, autoResize, builtinCommands, sendMessage } from './modules/input.js';
@@ -18,10 +20,11 @@ import { initScheduler, resetScheduler, handleLoopRegistryUpdated, handleSchedul
18
20
  import { initAsciiLogo, startLogoAnimation, stopLogoAnimation } from './modules/ascii-logo.js';
19
21
  import { initPlaybook, openPlaybook, getPlaybooks, getPlaybookForTip, isCompleted as isPlaybookCompleted } from './modules/playbook.js';
20
22
  import { initSTT } from './modules/stt.js';
21
- import { initProfile } from './modules/profile.js';
23
+ import { initProfile, getProfileLang } from './modules/profile.js';
22
24
  import { initAdmin, checkAdminAccess } from './modules/admin.js';
23
25
  import { initSessionSearch, toggleSearch, closeSearch, isSearchOpen, handleFindInSessionResults, onHistoryPrepended as onSessionSearchHistoryPrepended } from './modules/session-search.js';
24
26
  import { initTooltips, registerTooltip } from './modules/tooltip.js';
27
+ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } from './modules/mate-wizard.js';
25
28
 
26
29
  // --- Base path for multi-project routing ---
27
30
  var slugMatch = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
@@ -62,6 +65,11 @@ import { initTooltips, registerTooltip } from './modules/tooltip.js';
62
65
  var cachedDmFavorites = [];
63
66
  var cachedDmConversations = [];
64
67
  var dmRemovedUsers = {}; // { userId: true } - users explicitly removed from favorites
68
+ var cachedMatesList = []; // Cached list of mates for user strip
69
+
70
+ // --- Mate WS (separate connection to mate project) ---
71
+ var mateWs = null;
72
+ var mateProjectSlug = null;
65
73
 
66
74
  // --- Home Hub ---
67
75
  var homeHub = $("home-hub");
@@ -572,13 +580,24 @@ import { initTooltips, registerTooltip } from './modules/tooltip.js';
572
580
  // Hide sticky notes if visible
573
581
  hideNotes();
574
582
 
583
+ var isMate = targetUser && targetUser.isMate;
584
+
575
585
  // Hide project UI + sidebar, show DM UI
576
586
  var mainCol = document.getElementById("main-column");
577
- if (mainCol) mainCol.classList.add("dm-mode");
587
+ if (mainCol && !isMate) mainCol.classList.add("dm-mode");
578
588
  var sidebarCol = document.getElementById("sidebar-column");
579
589
  if (sidebarCol) sidebarCol.classList.add("dm-mode");
580
590
  var resizeHandle = document.getElementById("sidebar-resize-handle");
581
591
  if (resizeHandle) resizeHandle.classList.add("dm-mode");
592
+ if (isMate && targetUser.projectSlug) {
593
+ // Mate DM: connect to mate's own project via separate WS
594
+ // Main column stays visible (regular project chat UI), sidebar swaps to mate sidebar
595
+ showMateSidebar(targetUser.id, targetUser);
596
+ connectMateWs(targetUser.projectSlug);
597
+ // Hide terminal button (not relevant for mate)
598
+ var termBtn = document.getElementById("terminal-toggle-btn");
599
+ if (termBtn) termBtn.style.display = "none";
600
+ }
582
601
 
583
602
  // Hide user-island (my avatar behind it becomes visible)
584
603
  var userIsland = document.getElementById("user-island");
@@ -611,6 +630,15 @@ import { initTooltips, registerTooltip } from './modules/tooltip.js';
611
630
  if (dmHeaderBar && targetUser.avatarColor) {
612
631
  dmHeaderBar.style.background = targetUser.avatarColor;
613
632
  }
633
+ // Add mate tag if this is a mate
634
+ var existingTag = dmHeaderBar ? dmHeaderBar.querySelector(".dm-header-mate-tag") : null;
635
+ if (existingTag) existingTag.remove();
636
+ if (targetUser.isMate && dmHeaderBar) {
637
+ var mateTag = document.createElement("span");
638
+ mateTag.className = "dm-header-mate-tag";
639
+ mateTag.textContent = "MATE";
640
+ dmHeaderBar.appendChild(mateTag);
641
+ }
614
642
  }
615
643
  }
616
644
 
@@ -627,10 +655,24 @@ import { initTooltips, registerTooltip } from './modules/tooltip.js';
627
655
  if (sidebarCol) sidebarCol.classList.remove("dm-mode");
628
656
  var resizeHandle = document.getElementById("sidebar-resize-handle");
629
657
  if (resizeHandle) resizeHandle.classList.remove("dm-mode");
658
+ hideMateSidebar();
659
+ hideKnowledge();
660
+ disconnectMateWs();
661
+ // Restore terminal button
662
+ var termBtn = document.getElementById("terminal-toggle-btn");
663
+ if (termBtn) termBtn.style.display = "";
664
+ // Re-request session list from main project to refresh state
665
+ if (ws && ws.readyState === 1) {
666
+ ws.send(JSON.stringify({ type: "switch_session", id: activeSessionId }));
667
+ }
630
668
 
631
669
  // Reset DM header
632
670
  var dmHeaderBar = document.getElementById("dm-header-bar");
633
- if (dmHeaderBar) dmHeaderBar.style.background = "";
671
+ if (dmHeaderBar) {
672
+ dmHeaderBar.style.background = "";
673
+ var mateTag = dmHeaderBar.querySelector(".dm-header-mate-tag");
674
+ if (mateTag) mateTag.remove();
675
+ }
634
676
 
635
677
  // Restore user-island (covers my avatar again)
636
678
  var userIsland = document.getElementById("user-island");
@@ -641,6 +683,138 @@ import { initTooltips, registerTooltip } from './modules/tooltip.js';
641
683
  renderProjectList();
642
684
  }
643
685
 
686
+ function handleMateCreatedInApp(mate) {
687
+ if (!mate) return;
688
+ cachedMatesList.push(mate);
689
+ renderUserStrip(cachedAllUsers, cachedOnlineIds, myUserId, cachedDmFavorites, cachedDmConversations, dmUnread, dmRemovedUsers, cachedMatesList);
690
+ // Store pending interview data so we can auto-send after DM opens
691
+ pendingMateInterview = mate;
692
+ openDm(mate.id);
693
+ }
694
+
695
+ var pendingMateInterview = null;
696
+
697
+ function buildMateInterviewPrompt(mate) {
698
+ var sd = mate.seedData || {};
699
+ var parts = [];
700
+ var spokenLang = getProfileLang() || "en-US";
701
+ parts.push("Spoken Language: " + spokenLang);
702
+ if (sd.relationship) parts.push("Relationship: " + sd.relationship);
703
+ if (sd.activity && sd.activity.length > 0) parts.push("Activities: " + sd.activity.join(", "));
704
+ if (sd.communicationStyle && sd.communicationStyle.length > 0) {
705
+ var styleLabels = {
706
+ direct_concise: "direct and concise",
707
+ soft_detailed: "soft and detailed",
708
+ witty: "witty",
709
+ encouraging: "encouraging",
710
+ formal: "formal",
711
+ no_nonsense: "no-nonsense",
712
+ };
713
+ var styles = sd.communicationStyle.map(function (s) { return styleLabels[s] || s.replace(/_/g, " "); });
714
+ parts.push("Communication: " + styles.join(", "));
715
+ }
716
+ if (sd.autonomy) parts.push("Autonomy: " + sd.autonomy.replace(/_/g, " "));
717
+
718
+ return "Use the /clay-mate-interview skill to start the interview.\n\n" +
719
+ "Mate ID: " + mate.id + "\n" +
720
+ "Mate Directory: .claude/mates/" + mate.id + "\n\n" +
721
+ "Seed Data:\n" + parts.join("\n");
722
+ }
723
+
724
+ // --- Mate WS connection ---
725
+ var savedMainWs = null; // stash main project ws while in mate DM
726
+
727
+ function connectMateWs(slug) {
728
+ disconnectMateWs();
729
+ mateProjectSlug = slug;
730
+ var protocol = location.protocol === "https:" ? "wss:" : "ws:";
731
+ mateWs = new WebSocket(protocol + "//" + location.host + "/p/" + slug + "/ws");
732
+
733
+ mateWs.onopen = function () {
734
+ // Swap main ws to mateWs so all UI (input, model selector, etc.) routes through mate project
735
+ savedMainWs = ws;
736
+ ws = mateWs;
737
+ connected = true;
738
+ };
739
+
740
+ mateWs.onmessage = function (ev) {
741
+ var msg;
742
+ try { msg = JSON.parse(ev.data); } catch (e) { return; }
743
+ // Intercept session_list for mate sidebar
744
+ if (msg.type === "init" && msg.sessions) {
745
+ renderMateSessionList(msg.sessions);
746
+ requestKnowledgeList();
747
+ }
748
+ if (msg.type === "session_list") {
749
+ renderMateSessionList(msg.sessions || []);
750
+ }
751
+ // Intercept knowledge messages
752
+ if (msg.type === "knowledge_list") {
753
+ renderKnowledgeList(msg.files);
754
+ return;
755
+ }
756
+ if (msg.type === "knowledge_content") {
757
+ handleKnowledgeContent(msg);
758
+ return;
759
+ }
760
+ if (msg.type === "knowledge_saved" || msg.type === "knowledge_deleted") {
761
+ return; // list update follows separately
762
+ }
763
+ // On done: scan DOM for [[MATE_READY: name]], update name, strip marker
764
+ if (msg.type === "done" && dmTargetUser && dmTargetUser.isMate) {
765
+ // Let processMessage render first, then scan DOM
766
+ setTimeout(function () {
767
+ var fullText = messagesEl ? messagesEl.textContent : "";
768
+ var readyMatch = fullText.match(/\[\[MATE_READY:\s*(.+?)\]\]/);
769
+ if (readyMatch) {
770
+ var newName = readyMatch[1].trim();
771
+ dmTargetUser.displayName = newName;
772
+ updateMateSidebarProfile({ profile: { displayName: newName, avatarColor: dmTargetUser.avatarColor, avatarStyle: dmTargetUser.avatarStyle, avatarSeed: dmTargetUser.avatarSeed } });
773
+ if (savedMainWs && savedMainWs.readyState === 1) {
774
+ savedMainWs.send(JSON.stringify({
775
+ type: "mate_update",
776
+ mateId: dmTargetUser.id,
777
+ updates: { name: newName, status: "ready", profile: { displayName: newName } },
778
+ }));
779
+ }
780
+ }
781
+ // Strip all MATE_READY markers from visible elements
782
+ var walker = document.createTreeWalker(messagesEl, NodeFilter.SHOW_TEXT, null, false);
783
+ var node;
784
+ while (node = walker.nextNode()) {
785
+ if (node.nodeValue.indexOf("[[MATE_READY:") !== -1) {
786
+ node.nodeValue = node.nodeValue.replace(/\[\[MATE_READY:\s*.+?\]\]/g, "").trim();
787
+ }
788
+ }
789
+ }, 100);
790
+ }
791
+ // Feed everything into the main message handler (renders text, tools, etc.)
792
+ processMessage(msg);
793
+ };
794
+
795
+ mateWs.onclose = function () {
796
+ if (ws === mateWs) {
797
+ ws = savedMainWs;
798
+ savedMainWs = null;
799
+ }
800
+ mateWs = null;
801
+ };
802
+ }
803
+
804
+ function disconnectMateWs() {
805
+ if (mateWs) {
806
+ // Restore main ws before closing
807
+ if (ws === mateWs && savedMainWs) {
808
+ ws = savedMainWs;
809
+ savedMainWs = null;
810
+ }
811
+ mateWs.onclose = null;
812
+ mateWs.close();
813
+ mateWs = null;
814
+ }
815
+ mateProjectSlug = null;
816
+ }
817
+
644
818
  function appendDmMessage(msg) {
645
819
  var isMe = msg.from === myUserId;
646
820
  var d = new Date(msg.ts);
@@ -858,7 +1032,7 @@ import { initTooltips, registerTooltip } from './modules/tooltip.js';
858
1032
  renderTopbarPresence(msg.serverUsers);
859
1033
  // Re-render user strip online dots even without allUsers update
860
1034
  if (!msg.allUsers && cachedAllUsers.length > 0) {
861
- renderUserStrip(cachedAllUsers, cachedOnlineIds, myUserId, cachedDmFavorites, cachedDmConversations, dmUnread, dmRemovedUsers);
1035
+ renderUserStrip(cachedAllUsers, cachedOnlineIds, myUserId, cachedDmFavorites, cachedDmConversations, dmUnread, dmRemovedUsers, cachedMatesList);
862
1036
  }
863
1037
  }
864
1038
  // Update user strip (DM targets) in icon strip
@@ -866,7 +1040,7 @@ import { initTooltips, registerTooltip } from './modules/tooltip.js';
866
1040
  cachedAllUsers = msg.allUsers;
867
1041
  if (msg.dmFavorites) cachedDmFavorites = msg.dmFavorites;
868
1042
  if (msg.dmConversations) cachedDmConversations = msg.dmConversations;
869
- renderUserStrip(msg.allUsers, cachedOnlineIds, myUserId, cachedDmFavorites, cachedDmConversations, dmUnread, dmRemovedUsers);
1043
+ renderUserStrip(msg.allUsers, cachedOnlineIds, myUserId, cachedDmFavorites, cachedDmConversations, dmUnread, dmRemovedUsers, cachedMatesList);
870
1044
  // Render my avatar (always present, hidden behind user-island)
871
1045
  var meEl = document.getElementById("icon-strip-me");
872
1046
  if (meEl && !meEl.hasChildNodes()) {
@@ -903,8 +1077,10 @@ import { initTooltips, registerTooltip } from './modules/tooltip.js';
903
1077
  }
904
1078
 
905
1079
  function renderProjectList() {
906
- // Render icon strip projects
907
- var iconStripProjects = cachedProjects.map(function (p) {
1080
+ // Render icon strip projects (exclude mate projects)
1081
+ var iconStripProjects = cachedProjects.filter(function (p) {
1082
+ return !p.isMate;
1083
+ }).map(function (p) {
908
1084
  return {
909
1085
  slug: p.slug,
910
1086
  name: p.title || p.project,
@@ -1258,6 +1434,7 @@ import { initTooltips, registerTooltip } from './modules/tooltip.js';
1258
1434
  get myUserId() { return myUserId; },
1259
1435
  get projectOwnerId() { return currentProjectOwnerId; },
1260
1436
  openDm: function (userId) { openDm(userId); },
1437
+ openMateWizard: function () { requireClayMateInterview(function () { openMateWizard(); }); },
1261
1438
  openAddProjectModal: function () { openAddProjectModal(); },
1262
1439
  sendWs: function (msg) { if (ws && ws.readyState === 1) ws.send(JSON.stringify(msg)); },
1263
1440
  onDmRemoveUser: function (userId) { dmRemovedUsers[userId] = true; },
@@ -1265,6 +1442,12 @@ import { initTooltips, registerTooltip } from './modules/tooltip.js';
1265
1442
  };
1266
1443
  initSidebar(sidebarCtx);
1267
1444
  initIconStrip(sidebarCtx);
1445
+ initMateSidebar(function () { return mateWs; });
1446
+ initMateKnowledge(function () { return mateWs; });
1447
+ initMateWizard(
1448
+ function (msg) { if (ws && ws.readyState === 1) ws.send(JSON.stringify(msg)); },
1449
+ function (mate) { handleMateCreatedInApp(mate); }
1450
+ );
1268
1451
 
1269
1452
  // --- Connect overlay (animated ASCII logo) ---
1270
1453
  var asciiLogoCanvas = $("ascii-logo-canvas");
@@ -2914,6 +3097,11 @@ import { initTooltips, registerTooltip } from './modules/tooltip.js';
2914
3097
  }));
2915
3098
  } catch(e) {}
2916
3099
  }
3100
+
3101
+ // Request mates list
3102
+ try {
3103
+ ws.send(JSON.stringify({ type: "mate_list" }));
3104
+ } catch(e) {}
2917
3105
  };
2918
3106
 
2919
3107
  ws.onclose = function (e) {
@@ -3717,7 +3905,26 @@ import { initTooltips, registerTooltip } from './modules/tooltip.js';
3717
3905
 
3718
3906
  // --- DM ---
3719
3907
  case "dm_history":
3908
+ // Attach projectSlug to targetUser for mate DMs
3909
+ if (msg.projectSlug && msg.targetUser) {
3910
+ msg.targetUser.projectSlug = msg.projectSlug;
3911
+ }
3720
3912
  enterDmMode(msg.dmKey, msg.targetUser, msg.messages);
3913
+ // Auto-send first interview prompt after mate DM opens
3914
+ if (pendingMateInterview && msg.targetUser && msg.targetUser.isMate && msg.projectSlug) {
3915
+ var interviewMate = pendingMateInterview;
3916
+ pendingMateInterview = null;
3917
+ // Wait for mateWs to be swapped in as main ws, then send interview prompt
3918
+ var checkMateReady = setInterval(function () {
3919
+ if (ws && ws === mateWs && ws.readyState === 1) {
3920
+ clearInterval(checkMateReady);
3921
+ var interviewText = buildMateInterviewPrompt(interviewMate);
3922
+ // Send through normal input flow (ws is now mateWs)
3923
+ ws.send(JSON.stringify({ type: "message", text: interviewText }));
3924
+ }
3925
+ }, 100);
3926
+ setTimeout(function () { clearInterval(checkMateReady); }, 5000);
3927
+ }
3721
3928
  break;
3722
3929
 
3723
3930
  case "dm_message":
@@ -3731,7 +3938,7 @@ import { initTooltips, registerTooltip } from './modules/tooltip.js';
3731
3938
  if (fromId && fromId !== myUserId) {
3732
3939
  dmUnread[fromId] = (dmUnread[fromId] || 0) + 1;
3733
3940
  // Re-render strip so non-favorited sender appears
3734
- renderUserStrip(cachedAllUsers, cachedOnlineIds, myUserId, cachedDmFavorites, cachedDmConversations, dmUnread, dmRemovedUsers);
3941
+ renderUserStrip(cachedAllUsers, cachedOnlineIds, myUserId, cachedDmFavorites, cachedDmConversations, dmUnread, dmRemovedUsers, cachedMatesList);
3735
3942
  updateDmBadge(fromId, dmUnread[fromId]);
3736
3943
  }
3737
3944
  }
@@ -3763,7 +3970,55 @@ import { initTooltips, registerTooltip } from './modules/tooltip.js';
3763
3970
  }
3764
3971
  }
3765
3972
  cachedDmFavorites = msg.dmFavorites || [];
3766
- renderUserStrip(cachedAllUsers, cachedOnlineIds, myUserId, cachedDmFavorites, cachedDmConversations, dmUnread, dmRemovedUsers);
3973
+ renderUserStrip(cachedAllUsers, cachedOnlineIds, myUserId, cachedDmFavorites, cachedDmConversations, dmUnread, dmRemovedUsers, cachedMatesList);
3974
+ break;
3975
+
3976
+ case "mate_created":
3977
+ handleMateCreatedInApp(msg.mate);
3978
+ break;
3979
+
3980
+ case "mate_deleted":
3981
+ cachedMatesList = cachedMatesList.filter(function (m) { return m.id !== msg.mateId; });
3982
+ renderUserStrip(cachedAllUsers, cachedOnlineIds, myUserId, cachedDmFavorites, cachedDmConversations, dmUnread, dmRemovedUsers, cachedMatesList);
3983
+ // If currently in DM with this mate, exit DM mode
3984
+ if (dmMode && dmTargetUser && dmTargetUser.id === msg.mateId) {
3985
+ exitDmMode();
3986
+ }
3987
+ break;
3988
+
3989
+ case "mate_updated":
3990
+ if (msg.mate) {
3991
+ for (var mi = 0; mi < cachedMatesList.length; mi++) {
3992
+ if (cachedMatesList[mi].id === msg.mate.id) {
3993
+ cachedMatesList[mi] = msg.mate;
3994
+ break;
3995
+ }
3996
+ }
3997
+ renderUserStrip(cachedAllUsers, cachedOnlineIds, myUserId, cachedDmFavorites, cachedDmConversations, dmUnread, dmRemovedUsers, cachedMatesList);
3998
+ // Update mate sidebar if currently viewing this mate
3999
+ if (dmMode && dmTargetUser && dmTargetUser.isMate && dmTargetUser.id === msg.mate.id) {
4000
+ updateMateSidebarProfile(msg.mate);
4001
+ }
4002
+ // Update DM header if currently chatting with this mate
4003
+ if (dmMode && currentDmTarget === msg.mate.id) {
4004
+ var updatedName = (msg.mate.profile && msg.mate.profile.displayName) || msg.mate.name;
4005
+ if (updatedName) {
4006
+ var dmHeaderName = document.getElementById("dm-header-name");
4007
+ if (dmHeaderName) dmHeaderName.textContent = updatedName;
4008
+ var dmInput = document.getElementById("dm-input");
4009
+ if (dmInput) dmInput.placeholder = "Message " + updatedName;
4010
+ }
4011
+ }
4012
+ }
4013
+ break;
4014
+
4015
+ case "mate_list":
4016
+ cachedMatesList = msg.mates || [];
4017
+ renderUserStrip(cachedAllUsers, cachedOnlineIds, myUserId, cachedDmFavorites, cachedDmConversations, dmUnread, dmRemovedUsers, cachedMatesList);
4018
+ break;
4019
+
4020
+ case "mate_error":
4021
+ showToast(msg.error || "Mate operation failed", "error");
3767
4022
  break;
3768
4023
 
3769
4024
  case "daemon_config":
@@ -4074,7 +4329,7 @@ import { initTooltips, registerTooltip } from './modules/tooltip.js';
4074
4329
  showImageModal: showImageModal,
4075
4330
  hideSuggestionChips: hideSuggestionChips,
4076
4331
  setSendBtnMode: setSendBtnMode,
4077
- isDmMode: function () { return dmMode; },
4332
+ isDmMode: function () { return dmMode && !(dmTargetUser && dmTargetUser.isMate); },
4078
4333
  getDmKey: function () { return dmKey; },
4079
4334
  handleDmSend: function () { handleDmSend(); },
4080
4335
  });
@@ -4685,6 +4940,14 @@ import { initTooltips, registerTooltip } from './modules/tooltip.js';
4685
4940
  }, cb);
4686
4941
  }
4687
4942
 
4943
+ function requireClayMateInterview(cb) {
4944
+ requireSkills({
4945
+ title: "Skill Installation Required",
4946
+ reason: "The Mate Interview skill is required to create a new Mate.",
4947
+ skills: [{ name: "clay-mate-interview", url: "https://github.com/chadbyte/clay-mate-interview", scope: "global" }]
4948
+ }, cb);
4949
+ }
4950
+
4688
4951
  // --- Ralph Wizard ---
4689
4952
 
4690
4953
  function openRalphWizard() {
@@ -235,6 +235,7 @@ html, body {
235
235
 
236
236
  /* --- Main Column --- */
237
237
  #main-column {
238
+ position: relative;
238
239
  flex: 1;
239
240
  display: flex;
240
241
  flex-direction: column;