(1/2) Cursor Chat - Presence (#1487)

This PR adds support for seeing **another user**'s chat messages.

It's part 1 of two PRs relating to Cursor Chat.
And it's needed for the much bigger part 2:
https://github.com/tldraw/brivate/pull/1981

# Presence

You can see another person's chat messages!

![2023-06-02 at 17 42 33 - Blush
Capybara](https://github.com/tldraw/tldraw/assets/15892272/8f3efb5f-9c05-459c-aa7e-24842be75e58)

If they have a name, it gets popped on top.

![2023-06-02 at 17 45 34 - Sapphire
Meerkat](https://github.com/tldraw/tldraw/assets/15892272/749bd924-c1f5-419b-a028-1fafe1b61292)

That's it!
With this PR, there's no way of actually *typing* your chat messages.
That comes with the [next
one](https://github.com/tldraw/brivate/pull/1981)!

# Admin

### To-do

- [x] Store chat message
- [x] Allow overflowing chat
- [x] Presence for chat message
- [x] Display chat message to others

### Change Type

- [x] `minor` — New Feature

### Test Plan

To test this, I recommend checking out both `lu/cursor-chat` branches,
and opening two browser sessions in the same shared project.

1. In one session, type some cursor chat by pressing the Enter key while
on the canvas (and typing).
2. On the other session, check that you can see the chat message appear.
3. Repeat this while being both named, and unnamed.

I recommend just focusing on the visible presense in this PR.
The [other PR](https://github.com/tldraw/brivate/pull/1981) is where we
can focus about how we _input_ the cursor chat.

### Release Notes

- [dev] Added support for cursor chat presence.

---------

Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
Lu Wilson 2023-06-15 16:10:08 +01:00 committed by GitHub
parent 21377c0f22
commit 3bbb34eba8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 238 additions and 39 deletions

View file

@ -1,15 +1,16 @@
/* eslint-disable no-inner-declarations */
import { InstancePresenceRecordType, Tldraw } from '@tldraw/tldraw' import { InstancePresenceRecordType, Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/editor.css' import '@tldraw/tldraw/editor.css'
import '@tldraw/tldraw/ui.css' import '@tldraw/tldraw/ui.css'
import { useRef } from 'react' import { useRef } from 'react'
const SHOW_MOVING_CURSOR = true const USER_NAME = 'huppy da arrow'
const CURSOR_SPEED = 0.5 const MOVING_CURSOR_SPEED = 0.25 // 0 is stopped, 1 is full send
const CIRCLE_RADIUS = 100 const MOVING_CURSOR_RADIUS = 100
const UPDATE_FPS = 60 const CURSOR_CHAT_MESSAGE = 'Hey, I think this is just great.'
export default function UserPresenceExample() { export default function UserPresenceExample() {
const rTimeout = useRef<any>(-1) const rRaf = useRef<any>(-1)
return ( return (
<div className="tldraw__editor"> <div className="tldraw__editor">
<Tldraw <Tldraw
@ -22,39 +23,62 @@ export default function UserPresenceExample() {
id: InstancePresenceRecordType.createId(editor.store.id), id: InstancePresenceRecordType.createId(editor.store.id),
currentPageId: editor.currentPageId, currentPageId: editor.currentPageId,
userId: 'peer-1', userId: 'peer-1',
userName: 'Peer 1', userName: USER_NAME,
cursor: { x: 0, y: 0, type: 'default', rotation: 0 }, cursor: { x: 0, y: 0, type: 'default', rotation: 0 },
chatMessage: CURSOR_CHAT_MESSAGE,
}) })
editor.store.put([peerPresence]) editor.store.put([peerPresence])
// Make the fake user's cursor rotate in a circle // Make the fake user's cursor rotate in a circle
if (rTimeout.current) { const raf = rRaf.current
clearTimeout(rTimeout.current) cancelAnimationFrame(raf)
}
if (MOVING_CURSOR_SPEED > 0 || CURSOR_CHAT_MESSAGE) {
function loop() {
let cursor = peerPresence.cursor
let chatMessage = peerPresence.chatMessage
if (SHOW_MOVING_CURSOR) {
rTimeout.current = setInterval(() => {
const k = 1000 / CURSOR_SPEED
const now = Date.now() const now = Date.now()
const t = (now % k) / k
// rotate in a circle if (MOVING_CURSOR_SPEED > 0) {
const k = 1000 / MOVING_CURSOR_SPEED
const t = (now % k) / k
cursor = {
...peerPresence.cursor,
x: 150 + Math.cos(t * Math.PI * 2) * MOVING_CURSOR_RADIUS,
y: 150 + Math.sin(t * Math.PI * 2) * MOVING_CURSOR_RADIUS,
}
}
if (CURSOR_CHAT_MESSAGE) {
const k = 1000
const t = (now % (k * 3)) / k
chatMessage =
t < 1
? ''
: t > 2
? CURSOR_CHAT_MESSAGE
: CURSOR_CHAT_MESSAGE.slice(0, Math.ceil((t - 1) * CURSOR_CHAT_MESSAGE.length))
}
editor.store.put([ editor.store.put([
{ {
...peerPresence, ...peerPresence,
cursor: { cursor,
...peerPresence.cursor, chatMessage,
x: 150 + Math.cos(t * Math.PI * 2) * CIRCLE_RADIUS,
y: 150 + Math.sin(t * Math.PI * 2) * CIRCLE_RADIUS,
},
lastActivityTimestamp: now, lastActivityTimestamp: now,
}, },
]) ])
}, 1000 / UPDATE_FPS)
rRaf.current = requestAnimationFrame(loop)
}
rRaf.current = requestAnimationFrame(loop)
} else { } else {
editor.store.put([{ ...peerPresence, lastActivityTimestamp: Date.now() }]) editor.store.put([{ ...peerPresence, lastActivityTimestamp: Date.now() }])
rRaf.current = setInterval(() => {
rTimeout.current = setInterval(() => {
editor.store.put([{ ...peerPresence, lastActivityTimestamp: Date.now() }]) editor.store.put([{ ...peerPresence, lastActivityTimestamp: Date.now() }])
}, 1000) }, 1000)
} }

View file

@ -46,6 +46,7 @@
"action.leave-shared-project": "Leave shared project", "action.leave-shared-project": "Leave shared project",
"action.new-project": "New project", "action.new-project": "New project",
"action.new-shared-project": "New shared project", "action.new-shared-project": "New shared project",
"action.open-cursor-chat": "Cursor chat",
"action.open-file": "Open file", "action.open-file": "Open file",
"action.pack": "Pack", "action.pack": "Pack",
"action.paste": "Paste", "action.paste": "Paste",
@ -278,6 +279,7 @@
"shortcuts-dialog.tools": "Tools", "shortcuts-dialog.tools": "Tools",
"shortcuts-dialog.transform": "Transform", "shortcuts-dialog.transform": "Transform",
"shortcuts-dialog.view": "View", "shortcuts-dialog.view": "View",
"shortcuts-dialog.collaboration": "Collaboration",
"home-project-dialog.title": "Home project", "home-project-dialog.title": "Home project",
"home-project-dialog.description": "This is your local home project. It's just for you!", "home-project-dialog.description": "This is your local home project. It's just for you!",
"rename-project-dialog.title": "Rename project", "rename-project-dialog.title": "Rename project",
@ -339,5 +341,6 @@
"vscode.file-open.backup": "Backup", "vscode.file-open.backup": "Backup",
"vscode.file-open.backup-saved": "Backup saved", "vscode.file-open.backup-saved": "Backup saved",
"vscode.file-open.backup-failed": "Backup failed: this is not a .tldr file.", "vscode.file-open.backup-failed": "Backup failed: this is not a .tldr file.",
"vscode.file-open.dont-show-again": "Don't ask again" "vscode.file-open.dont-show-again": "Don't ask again",
"cursor-chat.type-to-chat": "Type to chat..."
} }

View file

@ -62,6 +62,27 @@ https://alex.dytry.ch/toys/palette/?palette=%7B%22families%22:%5B%22black%22,%22
/* These cursor values get programmatically overridden */ /* These cursor values get programmatically overridden */
/* They're just here to help your editor autocomplete */ /* They're just here to help your editor autocomplete */
--tl-cursor: var(--tl-default-svg); --tl-cursor: var(--tl-default-svg);
--tl-cursor-none: none;
--tl-cursor-default: default;
--tl-cursor-pointer: pointer;
--tl-cursor-cross: crosshair;
--tl-cursor-move: move;
--tl-cursor-grab: grab;
--tl-cursor-grabbing: grabbing;
--tl-cursor-text: text;
--tl-cursor-resize-edge: ew-resize;
--tl-cursor-resize-corner: nesw-resize;
--tl-cursor-ew-resize: ew-resize;
--tl-cursor-ns-resize: ns-resize;
--tl-cursor-nesw-resize: nesw-resize;
--tl-cursor-nwse-resize: nwse-resize;
--tl-cursor-rotate: pointer;
--tl-cursor-nwse-rotate: pointer;
--tl-cursor-nesw-rotate: pointer;
--tl-cursor-senw-rotate: pointer;
--tl-cursor-swne-rotate: pointer;
--tl-cursor-zoom-in: zoom-in;
--tl-cursor-zoom-out: zoom-out;
--tl-scale: calc(1 / var(--tl-zoom)); --tl-scale: calc(1 / var(--tl-zoom));
--tl-font-draw: 'tldraw_draw', sans-serif; --tl-font-draw: 'tldraw_draw', sans-serif;
--tl-font-sans: 'tldraw_sans', sans-serif; --tl-font-sans: 'tldraw_sans', sans-serif;
@ -746,7 +767,6 @@ input,
position: absolute; position: absolute;
} }
/* Rounded corners */
.tl-nametag { .tl-nametag {
position: absolute; position: absolute;
top: 16px; top: 16px;
@ -754,15 +774,84 @@ input,
width: fit-content; width: fit-content;
height: fit-content; height: fit-content;
max-width: 120px; max-width: 120px;
color: var(--color-selected-contrast); padding: 3px 6px;
white-space: nowrap; white-space: nowrap;
position: absolute; position: absolute;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
border-radius: 10px;
padding: 2px 6px;
font-size: 12px; font-size: 12px;
font-family: var(--font-family); font-family: var(--font-family);
border-radius: var(--radius-2);
color: var(--color-selected-contrast);
}
.tl-nametag-title {
position: absolute;
top: -2px;
left: 13px;
width: fit-content;
height: fit-content;
padding: 0px 6px;
max-width: 120px;
white-space: nowrap;
position: absolute;
overflow: hidden;
text-overflow: ellipsis;
font-size: 12px;
font-family: var(--font-family);
text-shadow: var(--tl-text-outline);
color: var(--color-selected-contrast);
}
.tl-nametag-chat {
position: absolute;
top: 16px;
left: 13px;
width: fit-content;
height: fit-content;
color: var(--color-selected-contrast);
white-space: nowrap;
position: absolute;
padding: 3px 6px;
font-size: 12px;
font-family: var(--font-family);
opacity: 1;
border-radius: var(--radius-2);
}
.tl-cursor-chat {
position: absolute;
color: var(--color-selected-contrast);
white-space: nowrap;
padding: 3px 6px;
font-size: 12px;
font-family: var(--font-family);
pointer-events: none;
z-index: var(--layer-cursor);
margin-top: 16px;
margin-left: 13px;
opacity: 1;
border: none;
user-select: text;
border-radius: var(--radius-2);
}
.tl-cursor-chat::selection {
background: var(--color-selected);
color: var(--color-selected-contrast);
text-shadow: none;
}
.tl-cursor-chat-fade {
/* Setting to zero causes it to immediately disappear */
/* Setting to near-zero causes it to fade out gradually */
opacity: 0.0001;
transition: opacity 5s ease-in-out;
}
.tl-cursor-chat::placeholder {
color: var(--color-selected-contrast);
opacity: 0.7;
} }
/* -------------------------------------------------- */ /* -------------------------------------------------- */

View file

@ -10,23 +10,37 @@ export type TLCursorComponent = (props: {
zoom: number zoom: number
color?: string color?: string
name: string | null name: string | null
chatMessage: string
}) => any | null }) => any | null
const _Cursor: TLCursorComponent = ({ className, zoom, point, color, name }) => { const _Cursor: TLCursorComponent = ({ className, zoom, point, color, name, chatMessage }) => {
const rDiv = useRef<HTMLDivElement>(null) const rCursor = useRef<HTMLDivElement>(null)
useTransform(rDiv, point?.x, point?.y, 1 / zoom) useTransform(rCursor, point?.x, point?.y, 1 / zoom)
if (!point) return null if (!point) return null
return ( return (
<div ref={rDiv} className={classNames('tl-overlays__item', className)}> <div ref={rCursor} className={classNames('tl-overlays__item', className)}>
<svg className="tl-cursor"> <svg className="tl-cursor">
<use href="#cursor" color={color} /> <use href="#cursor" color={color} />
</svg> </svg>
{name !== null && name !== '' && ( {chatMessage ? (
<div className="tl-nametag" style={{ backgroundColor: color }}> <>
{name} {name && (
</div> <div className="tl-nametag-title" style={{ color }}>
{name}
</div>
)}
<div className="tl-nametag-chat" style={{ backgroundColor: color }}>
{chatMessage}
</div>
</>
) : (
name && (
<div className="tl-nametag" style={{ backgroundColor: color }}>
{name}
</div>
)
)} )}
</div> </div>
) )

View file

@ -33,7 +33,7 @@ const Collaborator = track(function Collaborator({ userId }: { userId: string })
// if the collaborator is on another page, ignore them // if the collaborator is on another page, ignore them
if (latestPresence.currentPageId !== editor.currentPageId) return null if (latestPresence.currentPageId !== editor.currentPageId) return null
const { brush, scribble, selectedIds, userName, cursor, color } = latestPresence const { brush, scribble, selectedIds, userName, cursor, color, chatMessage } = latestPresence
// Add a little padding to the top-left of the viewport // Add a little padding to the top-left of the viewport
// so that the cursor doesn't get cut off // so that the cursor doesn't get cut off
@ -63,6 +63,7 @@ const Collaborator = track(function Collaborator({ userId }: { userId: string })
color={color} color={color}
zoom={zoomLevel} zoom={zoomLevel}
name={userName !== 'New User' ? userName : null} name={userName !== 'New User' ? userName : null}
chatMessage={chatMessage}
/> />
) : CollaboratorHint ? ( ) : CollaboratorHint ? (
<CollaboratorHint <CollaboratorHint

View file

@ -941,6 +941,8 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
// (undocumented) // (undocumented)
brush: Box2dModel | null; brush: Box2dModel | null;
// (undocumented) // (undocumented)
chatMessage: string;
// (undocumented)
currentPageId: TLPageId; currentPageId: TLPageId;
// (undocumented) // (undocumented)
cursor: TLCursor; cursor: TLCursor;
@ -949,6 +951,8 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
// (undocumented) // (undocumented)
followingUserId: null | string; followingUserId: null | string;
// (undocumented) // (undocumented)
isChatting: boolean;
// (undocumented)
isDebugMode: boolean; isDebugMode: boolean;
// (undocumented) // (undocumented)
isFocusMode: boolean; isFocusMode: boolean;
@ -1007,6 +1011,8 @@ export interface TLInstancePresence extends BaseRecord<'instance_presence', TLIn
z: number; z: number;
}; };
// (undocumented) // (undocumented)
chatMessage: string;
// (undocumented)
color: string; color: string;
// (undocumented) // (undocumented)
currentPageId: TLPageId; currentPageId: TLPageId;

View file

@ -43,6 +43,7 @@ export const createPresenceStateDerivation =
}, },
lastActivityTimestamp: pointer.lastActivityTimestamp, lastActivityTimestamp: pointer.lastActivityTimestamp,
screenBounds: instance.screenBounds, screenBounds: instance.screenBounds,
chatMessage: instance.chatMessage,
}) })
}) })
} }

View file

@ -1110,6 +1110,30 @@ describe('hoist opacity', () => {
}) })
}) })
describe('Adds chat message to presence', () => {
const { up, down } = instancePresenceMigrations.migrators[3]
test('up adds the chatMessage property', () => {
expect(up({})).toEqual({ chatMessage: '' })
})
test('down removes the chatMessage property', () => {
expect(down({ chatMessage: '' })).toEqual({})
})
})
describe('Adds chat properties to instance', () => {
const { up, down } = instanceMigrations.migrators[14]
test('up adds the chatMessage property', () => {
expect(up({})).toEqual({ chatMessage: '', isChatting: false })
})
test('down removes the chatMessage property', () => {
expect(down({ chatMessage: '', isChatting: true })).toEqual({})
})
})
describe('Removes does resize from embed', () => { describe('Removes does resize from embed', () => {
const { up, down } = embedShapeMigrations.migrators[2] const { up, down } = embedShapeMigrations.migrators[2]
test('up works as expected', () => { test('up works as expected', () => {

View file

@ -44,6 +44,10 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
exportBackground: boolean exportBackground: boolean
screenBounds: Box2dModel screenBounds: Box2dModel
zoomBrush: Box2dModel | null zoomBrush: Box2dModel | null
chatMessage: string
isChatting: boolean
isPenMode: boolean isPenMode: boolean
isGridMode: boolean isGridMode: boolean
} }
@ -87,6 +91,8 @@ export const instanceTypeValidator: T.Validator<TLInstance> = T.model(
exportBackground: T.boolean, exportBackground: T.boolean,
screenBounds: T.boxModel, screenBounds: T.boxModel,
zoomBrush: T.boxModel.nullable(), zoomBrush: T.boxModel.nullable(),
chatMessage: T.string,
isChatting: T.boolean,
isPenMode: T.boolean, isPenMode: T.boolean,
isGridMode: T.boolean, isGridMode: T.boolean,
}) })
@ -106,13 +112,14 @@ const Versions = {
RemoveUserId: 11, RemoveUserId: 11,
AddIsPenModeAndIsGridMode: 12, AddIsPenModeAndIsGridMode: 12,
HoistOpacity: 13, HoistOpacity: 13,
AddChat: 14,
} as const } as const
export { Versions as instanceTypeVersions } export { Versions as instanceTypeVersions }
/** @public */ /** @public */
export const instanceMigrations = defineMigrations({ export const instanceMigrations = defineMigrations({
currentVersion: Versions.HoistOpacity, currentVersion: Versions.AddChat,
migrators: { migrators: {
[Versions.AddTransparentExportBgs]: { [Versions.AddTransparentExportBgs]: {
up: (instance: TLInstance) => { up: (instance: TLInstance) => {
@ -281,6 +288,14 @@ export const instanceMigrations = defineMigrations({
} }
}, },
}, },
[Versions.AddChat]: {
up: (instance: TLInstance) => {
return { ...instance, chatMessage: '', isChatting: false }
},
down: ({ chatMessage: _, isChatting: __, ...instance }: TLInstance) => {
return instance
},
},
}, },
}) })
@ -321,6 +336,8 @@ export const InstanceRecordType = createRecordType<TLInstance>('instance', {
isToolLocked: false, isToolLocked: false,
screenBounds: { x: 0, y: 0, w: 1080, h: 720 }, screenBounds: { x: 0, y: 0, w: 1080, h: 720 },
zoomBrush: null, zoomBrush: null,
chatMessage: '',
isChatting: false,
isGridMode: false, isGridMode: false,
isPenMode: false, isPenMode: false,
}) })

View file

@ -27,6 +27,7 @@ export interface TLInstancePresence extends BaseRecord<'instance_presence', TLIn
type: TLCursor['type'] type: TLCursor['type']
rotation: number rotation: number
} }
chatMessage: string
} }
/** @public */ /** @public */
@ -59,18 +60,20 @@ export const instancePresenceValidator: T.Validator<TLInstancePresence> = T.mode
currentPageId: idValidator<TLPageId>('page'), currentPageId: idValidator<TLPageId>('page'),
brush: T.boxModel.nullable(), brush: T.boxModel.nullable(),
scribble: scribbleValidator.nullable(), scribble: scribbleValidator.nullable(),
chatMessage: T.string,
}) })
) )
const Versions = { const Versions = {
AddScribbleDelay: 1, AddScribbleDelay: 1,
RemoveInstanceId: 2, RemoveInstanceId: 2,
AddChatMessage: 3,
} as const } as const
export { Versions as instancePresenceVersions } export { Versions as instancePresenceVersions }
export const instancePresenceMigrations = defineMigrations({ export const instancePresenceMigrations = defineMigrations({
currentVersion: Versions.RemoveInstanceId, currentVersion: Versions.AddChatMessage,
migrators: { migrators: {
[Versions.AddScribbleDelay]: { [Versions.AddScribbleDelay]: {
up: (instance) => { up: (instance) => {
@ -95,6 +98,14 @@ export const instancePresenceMigrations = defineMigrations({
return { ...instance, instanceId: TLINSTANCE_ID } return { ...instance, instanceId: TLINSTANCE_ID }
}, },
}, },
[Versions.AddChatMessage]: {
up: (instance) => {
return { ...instance, chatMessage: '' }
},
down: ({ chatMessage: _, ...instance }) => {
return instance
},
},
}, },
}) })
@ -130,4 +141,5 @@ export const InstancePresenceRecordType = createRecordType<TLInstancePresence>(
selectedIds: [], selectedIds: [],
brush: null, brush: null,
scribble: null, scribble: null,
chatMessage: '',
})) }))

File diff suppressed because one or more lines are too long

View file

@ -81,6 +81,7 @@ export interface TLUiEventMap {
'toggle-reduce-motion': null 'toggle-reduce-motion': null
'exit-pen-mode': null 'exit-pen-mode': null
'stop-following': null 'stop-following': null
'open-cursor-chat': null
} }
type Join<T, K> = K extends null type Join<T, K> = K extends null

View file

@ -50,6 +50,7 @@ export type TLUiTranslationKey =
| 'action.leave-shared-project' | 'action.leave-shared-project'
| 'action.new-project' | 'action.new-project'
| 'action.new-shared-project' | 'action.new-shared-project'
| 'action.open-cursor-chat'
| 'action.open-file' | 'action.open-file'
| 'action.pack' | 'action.pack'
| 'action.paste' | 'action.paste'
@ -282,6 +283,7 @@ export type TLUiTranslationKey =
| 'shortcuts-dialog.tools' | 'shortcuts-dialog.tools'
| 'shortcuts-dialog.transform' | 'shortcuts-dialog.transform'
| 'shortcuts-dialog.view' | 'shortcuts-dialog.view'
| 'shortcuts-dialog.collaboration'
| 'home-project-dialog.title' | 'home-project-dialog.title'
| 'home-project-dialog.description' | 'home-project-dialog.description'
| 'rename-project-dialog.title' | 'rename-project-dialog.title'
@ -344,3 +346,4 @@ export type TLUiTranslationKey =
| 'vscode.file-open.backup-saved' | 'vscode.file-open.backup-saved'
| 'vscode.file-open.backup-failed' | 'vscode.file-open.backup-failed'
| 'vscode.file-open.dont-show-again' | 'vscode.file-open.dont-show-again'
| 'cursor-chat.type-to-chat'

View file

@ -50,6 +50,7 @@ export const DEFAULT_TRANSLATION = {
'action.leave-shared-project': 'Leave shared project', 'action.leave-shared-project': 'Leave shared project',
'action.new-project': 'New project', 'action.new-project': 'New project',
'action.new-shared-project': 'New shared project', 'action.new-shared-project': 'New shared project',
'action.open-cursor-chat': 'Cursor chat',
'action.open-file': 'Open file', 'action.open-file': 'Open file',
'action.pack': 'Pack', 'action.pack': 'Pack',
'action.paste': 'Paste', 'action.paste': 'Paste',
@ -285,6 +286,7 @@ export const DEFAULT_TRANSLATION = {
'shortcuts-dialog.tools': 'Tools', 'shortcuts-dialog.tools': 'Tools',
'shortcuts-dialog.transform': 'Transform', 'shortcuts-dialog.transform': 'Transform',
'shortcuts-dialog.view': 'View', 'shortcuts-dialog.view': 'View',
'shortcuts-dialog.collaboration': 'Collaboration',
'home-project-dialog.title': 'Home project', 'home-project-dialog.title': 'Home project',
'home-project-dialog.description': "This is your local home project. It's just for you!", 'home-project-dialog.description': "This is your local home project. It's just for you!",
'rename-project-dialog.title': 'Rename project', 'rename-project-dialog.title': 'Rename project',
@ -354,4 +356,5 @@ export const DEFAULT_TRANSLATION = {
'vscode.file-open.backup-saved': 'Backup saved', 'vscode.file-open.backup-saved': 'Backup saved',
'vscode.file-open.backup-failed': 'Backup failed: this is not a .tldr file.', 'vscode.file-open.backup-failed': 'Backup failed: this is not a .tldr file.',
'vscode.file-open.dont-show-again': "Don't ask again", 'vscode.file-open.dont-show-again': "Don't ask again",
'cursor-chat.type-to-chat': 'Type to chat...',
} }

View file

@ -3,6 +3,7 @@
--layer-menus: 400; --layer-menus: 400;
--layer-overlays: 500; --layer-overlays: 500;
--layer-toasts: 650; --layer-toasts: 650;
--layer-cursor: 700;
} }
/* --------------------- Layout --------------------- */ /* --------------------- Layout --------------------- */