Skip to content

Welcome screen for daily notes#5056

Open
elstua wants to merge 4 commits intomainfrom
Welcome-screen-for-daily-notes
Open

Welcome screen for daily notes#5056
elstua wants to merge 4 commits intomainfrom
Welcome-screen-for-daily-notes

Conversation

@elstua
Copy link
Copy Markdown
Collaborator

@elstua elstua commented Apr 15, 2026

welcome screen in the app that tells user about what they can do and how it works

CleanShot 2026-04-14 at 22 33 10@2x

Note

Medium Risk
Introduces dynamic insertion/removal of a welcome block into the daily note editor and persists dismissal state in localStorage, which could affect note content serialization and user data if the strip/prepend logic misbehaves.

Overview
Adds a first-day Daily Notes welcome experience that conditionally shows a predefined welcomeContent at the top of today’s note, with a “Clear welcome message” control and persisted state via WELCOME_DATE_KEY/WELCOME_DISMISSED_KEY in localStorage.

Updates DailyNoteEditor to prepend the welcome nodes for display but strip them before saving, including cleanup for any previously persisted welcome content, and adds in-editor handling for char://settings/* links to open the corresponding settings tab.

Includes styling tweaks for the daily note editor (headings, task list spacing, session icon alignment, text-box-trim) plus minor spacing adjustments in session/task node views, and adds devtools/debug actions to reset the welcome state.

Reviewed by Cursor Bugbot for commit 3a8ff26. Bugbot is set up for automated code reviews on this repo. Configure here.

@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 15, 2026

Deploy Preview for char-cli-web canceled.

Name Link
🔨 Latest commit 3a8ff26
🔍 Latest deploy log https://app.netlify.com/projects/char-cli-web/deploys/69dfce8199155f0008ef0f06

@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 15, 2026

Deploy Preview for hyprnote canceled.

Name Link
🔨 Latest commit 3a8ff26
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote/deploys/69dfce82073fc400086cb555

@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 15, 2026

Deploy Preview for unsigned-char failed.

Name Link
🔨 Latest commit 3a8ff26
🔍 Latest deploy log https://app.netlify.com/projects/unsigned-char/deploys/69dfce81ea511000075191e6

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Fixed node count in stripWelcome risks silent data loss
    • Replaced hardcoded node count with separator-based search that handles user edits to welcome content structure, preventing data loss from incorrect slicing.
  • ✅ Fixed: Entire welcome-note.tsx file is unused dead code
    • Deleted the unused welcome-note.tsx file (265 lines) which was never imported anywhere in the codebase.

Create PR

Or push these changes by commenting:

@cursor push ec04264052
Preview (fastrepl/char@ec04264052)
diff --git a/apps/desktop/src/main2/home/note-editor.tsx b/apps/desktop/src/main2/home/note-editor.tsx
--- a/apps/desktop/src/main2/home/note-editor.tsx
+++ b/apps/desktop/src/main2/home/note-editor.tsx
@@ -170,28 +170,61 @@
   return JSON.stringify(firstContentNode) === JSON.stringify(firstWelcomeNode);
 }
 
-const WELCOME_NODE_COUNT = (welcomeContent.content?.length ?? 0) + 1; // +1 for separator paragraph
+const WELCOME_SEPARATOR_NODE = { type: "paragraph" };
 
 function prependWelcome(content: JSONContent): JSONContent {
   const welcomeNodes = welcomeContent.content ?? [];
   const existingNodes = content.content ?? [];
   return {
     type: "doc",
-    content: [...welcomeNodes, { type: "paragraph" }, ...existingNodes],
+    content: [...welcomeNodes, WELCOME_SEPARATOR_NODE, ...existingNodes],
   };
 }
 
 function stripWelcome(content: JSONContent): JSONContent {
-  let result = content;
-  while (hasWelcomeContent(result)) {
-    const nodes = result.content ?? [];
-    const remaining = nodes.slice(WELCOME_NODE_COUNT);
-    result = {
+  const nodes = content.content ?? [];
+  if (!hasWelcomeContent(content)) {
+    return content;
+  }
+
+  // Find the separator paragraph (empty paragraph after welcome content)
+  // and strip everything up to and including it
+  const welcomeNodesExpectedCount = welcomeContent.content?.length ?? 0;
+
+  // Look for the separator within a reasonable range (expected position +/- 5 nodes)
+  // to handle cases where user edited the welcome content
+  const searchStart = Math.max(0, welcomeNodesExpectedCount - 5);
+  const searchEnd = Math.min(nodes.length, welcomeNodesExpectedCount + 6);
+
+  let separatorIndex = -1;
+  for (let i = searchStart; i < searchEnd; i++) {
+    const node = nodes[i];
+    if (
+      node.type === "paragraph" &&
+      (!node.content || node.content.length === 0)
+    ) {
+      separatorIndex = i;
+      break;
+    }
+  }
+
+  // If we found the separator, strip up to and including it
+  if (separatorIndex >= 0) {
+    const remaining = nodes.slice(separatorIndex + 1);
+    return {
       type: "doc",
       content: remaining.length > 0 ? remaining : [{ type: "paragraph" }],
     };
   }
-  return result;
+
+  // Fallback: if no separator found but welcome content detected,
+  // strip based on expected count to avoid infinite loop
+  const expectedCount = (welcomeContent.content?.length ?? 0) + 1;
+  const remaining = nodes.slice(expectedCount);
+  return {
+    type: "doc",
+    content: remaining.length > 0 ? remaining : [{ type: "paragraph" }],
+  };
 }
 
 export function DailyNoteEditor({

diff --git a/apps/desktop/src/main2/home/welcome-note.tsx b/apps/desktop/src/main2/home/welcome-note.tsx
deleted file mode 100644
--- a/apps/desktop/src/main2/home/welcome-note.tsx
+++ /dev/null
@@ -1,265 +1,0 @@
-import "./note-editor.css";
-
-import { useCallback, useRef } from "react";
-
-import { parseJsonContent } from "@hypr/tiptap/shared";
-
-import {
-  type JSONContent,
-  NoteEditor,
-  type NoteEditorRef,
-} from "~/editor/session";
-import * as main from "~/store/tinybase/store/main";
-
-const WELCOME_ROW_ID = "welcome";
-
-const welcomeContent: JSONContent = {
-  type: "doc",
-  content: [
-    {
-      type: "heading",
-      attrs: { level: 2 },
-      content: [{ type: "text", text: "This is your daily notes" }],
-    },
-    {
-      type: "paragraph",
-      content: [
-        {
-          type: "text",
-          text: "Every day you get a fresh note for your task, ideas and meetings \u2014 write it any way you want and build your own workflow. Char took all context around your work and help you to fill your day.",
-        },
-      ],
-    },
-    {
-      type: "heading",
-      attrs: { level: 2 },
-      content: [{ type: "text", text: "Create lists, tasks and mentions" }],
-    },
-    {
-      type: "taskList",
-      content: [
-        {
-          type: "taskItem",
-          attrs: { checked: false },
-          content: [
-            {
-              type: "paragraph",
-              content: [
-                {
-                  type: "text",
-                  text: "You can create todo lists there. Try to type ",
-                },
-                { type: "text", marks: [{ type: "code" }], text: "-[]" },
-                { type: "text", text: " or just use " },
-                { type: "text", marks: [{ type: "code" }], text: "/" },
-                { type: "text", text: " for command" },
-              ],
-            },
-          ],
-        },
-      ],
-    },
-    {
-      type: "bulletList",
-      content: [
-        {
-          type: "listItem",
-          content: [
-            {
-              type: "paragraph",
-              content: [
-                { type: "text", text: "Write bullet lists and numbered one." },
-              ],
-            },
-          ],
-        },
-      ],
-    },
-    {
-      type: "heading",
-      attrs: { level: 2 },
-      content: [{ type: "text", text: "Check your meetings there" }],
-    },
-    {
-      type: "paragraph",
-      content: [
-        {
-          type: "text",
-          text: "All meetings from calendar appears in your daily notes. You can write notes inside the meetings or create new note",
-        },
-      ],
-    },
-    {
-      type: "session",
-      attrs: { sessionId: "welcome-demo", status: null, checked: null },
-      content: [
-        {
-          type: "paragraph",
-          content: [{ type: "text", text: "This is dummy meeting" }],
-        },
-      ],
-    },
-    {
-      type: "paragraph",
-      content: [
-        {
-          type: "text",
-          marks: [{ type: "italic" }],
-          text: "Create a new note by pressing ",
-        },
-        {
-          type: "text",
-          marks: [{ type: "code" }],
-          text: "new recording",
-        },
-        {
-          type: "text",
-          marks: [{ type: "italic" }],
-          text: " button",
-        },
-      ],
-    },
-    {
-      type: "heading",
-      attrs: { level: 2 },
-      content: [{ type: "text", text: "Make this fully yourself with AI" }],
-    },
-    {
-      type: "paragraph",
-      content: [
-        {
-          type: "text",
-          text: "Get the tasks done right from Daily Notes with multiple ",
-        },
-        {
-          type: "text",
-          marks: [
-            {
-              type: "link",
-              attrs: { href: "https://char.com/docs/integrations" },
-            },
-          ],
-          text: "integrations",
-        },
-        { type: "text", text: " Char has" },
-      ],
-    },
-    {
-      type: "heading",
-      attrs: { level: 2 },
-      content: [{ type: "text", text: "And much more" }],
-    },
-    {
-      type: "paragraph",
-      content: [
-        { type: "text", text: "Read " },
-        {
-          type: "text",
-          marks: [{ type: "link", attrs: { href: "https://char.com/docs" } }],
-          text: "docs",
-        },
-        {
-          type: "text",
-          text: " to integrate all features in your workflow. Join us on ",
-        },
-        {
-          type: "text",
-          marks: [{ type: "link", attrs: { href: "https://x.com/char" } }],
-          text: "X",
-        },
-        { type: "text", text: " or " },
-        {
-          type: "text",
-          marks: [{ type: "link", attrs: { href: "https://discord.gg/char" } }],
-          text: "Discord",
-        },
-        { type: "text", text: " to get the last updates" },
-      ],
-    },
-    {
-      type: "heading",
-      attrs: { level: 2 },
-      content: [
-        { type: "text", text: "All your previous notes perfectly fine" },
-      ],
-    },
-    {
-      type: "paragraph",
-      content: [
-        {
-          type: "text",
-          text: "They are available through previous Daily Notes, search and folder where you keep them, nothing changes",
-        },
-      ],
-    },
-  ],
-};
-
-function readSavedContent(
-  store: NonNullable<ReturnType<typeof main.UI.useStore>>,
-): JSONContent | null {
-  const cell = store.getCell("daily_notes", WELCOME_ROW_ID, "content");
-  if (typeof cell !== "string" || cell === "") return null;
-  return parseJsonContent(cell) ?? null;
-}
-
-export function WelcomeNote({ onDismiss }: { onDismiss: () => void }) {
-  const store = main.UI.useStore(main.STORE_ID);
-  const editorRef = useRef<NoteEditorRef>(null);
-
-  const initialContentRef = useRef<JSONContent | null>(null);
-  if (!initialContentRef.current && store) {
-    initialContentRef.current = readSavedContent(store) ?? welcomeContent;
-  }
-
-  const persistWelcomeNote = main.UI.useSetPartialRowCallback(
-    "daily_notes",
-    WELCOME_ROW_ID,
-    (input: JSONContent) => ({
-      content: JSON.stringify(input),
-      date: WELCOME_ROW_ID,
-    }),
-    [],
-    main.STORE_ID,
-  );
-
-  const handleChange = useCallback(
-    (input: JSONContent) => {
-      persistWelcomeNote(input);
-    },
-    [persistWelcomeNote],
-  );
-
-  if (!initialContentRef.current) {
-    return null;
-  }
-
-  return (
-    <div>
-      <div className="flex items-center gap-3 px-6 pt-6 pb-3">
-        <h2 className="text-xl font-semibold text-neutral-900">
-          Welcome to Char
-        </h2>
-      </div>
-
-      <div className="main2-daily-note-editor px-6">
-        <NoteEditor
-          ref={editorRef}
-          key="daily-welcome"
-          initialContent={initialContentRef.current}
-          handleChange={handleChange}
-          linkedItemOpenBehavior="new"
-        />
-      </div>
-
-      <div className="px-6 pt-4 pb-6">
-        <button
-          onClick={onDismiss}
-          className="rounded-full bg-neutral-900 px-5 py-2.5 text-sm font-medium text-white transition-colors hover:bg-neutral-800"
-        >
-          Start to use your Char
-        </button>
-      </div>
-    </div>
-  );
-}
\ No newline at end of file

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3129e3a. Configure here.

};
}
return result;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed node count in stripWelcome risks silent data loss

High Severity

stripWelcome uses a hardcoded WELCOME_NODE_COUNT (9) to slice nodes, but the welcome content is rendered in a fully editable ProseMirror editor with no read-only protection. If a user deletes a welcome paragraph or presses Enter to split a node, the actual node count drifts from the constant. When handleChange calls stripWelcome on every keystroke, nodes.slice(9) will silently discard the user's first real content nodes (if welcome nodes were removed) or leak welcome remnants into persisted data (if nodes were added). Similarly, if the user edits the first heading text, hasWelcomeContent returns false and the entire welcome content gets permanently persisted into the daily note.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3129e3a. Configure here.

</div>
</div>
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Entire welcome-note.tsx file is unused dead code

Medium Severity

welcome-note.tsx exports WelcomeNote but it is never imported anywhere in the codebase. It contains a separate 265-line implementation with its own welcomeContent and persistence logic (writing to a "welcome" row in daily_notes), which differs entirely from the approach actually used via welcome-content.ts and note-editor.tsx. This appears to be an abandoned earlier approach that was accidentally included in the PR.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3129e3a. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant