Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changeset/link-authoring-ux.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@inkeep/open-knowledge": minor
---

Link authoring in the visual editor now matches the conventions of the editors
you already know. Typing a full URL (`https://…`, `www.…`, or an email) and
pressing space or enter converts it to a link — including local-development
URLs like `http://localhost:5174` — while plain words and filenames like
`AGENTS.md` are never touched, and one undo restores plain text. Pasting or
dropping a lone URL links it; pasting a URL over selected text links that text
instead; `Cmd+Shift+V` still pastes plain. Typing markdown's `[text](url)`
shorthand converts on the closing parenthesis. `Cmd+K` is now dual-role: with
text selected it opens the link popover (focused, pre-filled from your
clipboard when it holds a URL), with the cursor inside a link it opens that
link for editing, and everywhere else it keeps opening the command palette.
Linkification never fires on content written by other collaborators or agents
— only on your own local typing and paste gestures. Also fixes two
long-standing popover bugs: the URL input now reliably takes focus on open,
and Escape correctly closes the popover after dismissing path suggestions.
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 11 additions & 1 deletion docs/content/features/editor.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Editor
icon: LuPenLine
description: The OpenKnowledge editor. WYSIWYG markdown, source toggle, content blocks, inline frontmatter, and the file sidebar.
description: The OpenKnowledge editor. WYSIWYG markdown, source toggle, content blocks, link authoring, inline frontmatter, and the file sidebar.
---
The editor is where you read, write, and collaborate on your docs, and where AI agents land when they edit. WYSIWYG by default with a source-mode toggle, a frontmatter properties pane, real-time CRDT collaboration, and a file sidebar that organizes the project.

Expand Down Expand Up @@ -40,6 +40,15 @@ An `html preview` code fence renders its contents live in a sandboxed iframe ins

`Cmd+F` opens find in the visual editor; `Cmd+Option+F` (`Ctrl+H` on Windows / Linux) adds the replace controls. `Cmd+G` and `Shift+Cmd+G` step through matches.

## Links

Links work the way they do in the editors you already know:

- **Type a URL, then press space or enter.** A full URL (`https://…`, `www.…`, or an email address) becomes a link when you press space or enter after it. Plain words and filenames like `AGENTS.md` are left alone, and one `Cmd+Z` turns a conversion back into plain text.
- **Paste or drag in a URL.** Pasted (or dropped) on its own, it becomes a link. Pasted over selected text, it links that text instead — the text stays, the URL becomes its destination. `Cmd+Shift+V` pastes plain, with no linking.
- **Type `[text](url)`.** Closing the parenthesis converts the markdown shorthand into a real link; `Cmd+Z` restores the literal text.
- **Press `Cmd+K` with text selected.** The link popover opens with the URL field focused, pre-filled from your clipboard when it holds a URL. With the cursor inside an existing link, `Cmd+K` opens that link for editing. With no selection, `Cmd+K` keeps opening the command palette, as everywhere else.

## Right-click menu and spell check

In the desktop app, right-clicking editable text opens a native context menu. Any editable field gets **Cut / Copy / Paste / Select All**; when there's a selection or a flagged word under the cursor, **Look Up** (the macOS dictionary panel) and **Search with Google** join it.
Expand Down Expand Up @@ -185,6 +194,7 @@ Keyboard shortcuts (use `Ctrl` on Windows / Linux in place of `Cmd`, unless the
| `Cmd+1` ... `Cmd+8` | Jump to one of the first eight editor tabs. |
| `Cmd+9` | Jump to the last editor tab. |
| `Cmd+Shift+T` | Reopen the most recently closed editor tab. |
| `Cmd+K` | Command palette — or, with text selected in the visual editor, add a link (cursor inside a link edits it). |
| `Cmd+Option+S` | Show / Hide the file sidebar (left). Matches Apple's standard sidebar accelerator. |
| `Cmd+Option+B` | Show / Hide the document panel (right). Matches VS Code's Secondary Side Bar accelerator. |
| `Cmd+J` | Show / Hide the docked terminal (desktop app). |
Expand Down
3 changes: 2 additions & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"measure:fuzz": "bash scripts/measure-fuzz.sh",
"measure:stress": "bash scripts/measure-stress.sh",
"perf:compare": "bash scripts/perf-compare.sh",
"test:e2e": "playwright test tests/stress/ux-interactions.e2e.ts tests/stress/file-tree-create.e2e.ts tests/stress/file-tree-drag-to-root.e2e.ts tests/stress/file-tree-deselect-to-root.e2e.ts tests/stress/file-tree-compact-folders.e2e.ts tests/stress/create-then-rename-editable.e2e.ts tests/stress/find-replace.e2e.ts tests/stress/editor-tabs.e2e.ts tests/stress/crdt-stress.e2e.ts tests/stress/slash-command.e2e.ts tests/stress/paste-fidelity.e2e.ts tests/stress/fr-7a-disconnect-source-mode.e2e.ts tests/stress/docs-open.e2e.ts tests/stress/frozen-table-headers.e2e.ts tests/stress/asset-embed.e2e.ts tests/stress/asset-embed-advanced.e2e.ts tests/stress/asset-click-dispatch.e2e.ts tests/stress/handoff.e2e.ts tests/stress/multi-agent-presence.e2e.ts tests/stress/editor-mode-persistence.e2e.ts tests/stress/sidebar-search-pill.e2e.ts tests/stress/command-palette-semantic.e2e.ts tests/stress/agent-activity-panel.e2e.ts tests/stress/drop-pipeline-auto-open.e2e.ts tests/stress/command-palette-flicker.e2e.ts tests/stress/cm6-list-hanging-indent.e2e.ts tests/stress/ng7-rapid-nav-coherence.e2e.ts tests/stress/reveal-on-activate.e2e.ts tests/stress/selection-indicator.e2e.ts tests/stress/new-file-cross-doc-bleed.e2e.ts tests/stress/editor-mode-flip-cross-doc-bleed.e2e.ts tests/stress/editor-area-viewport-resize.e2e.ts tests/stress/qa-sidebar-responsive.e2e.ts tests/stress/prd-6955-reassertion-repro.e2e.ts tests/stress/prd-6955-reassertion-wedge.e2e.ts tests/stress/prd-6914-repro.e2e.ts tests/stress/showall-lazy-tree.e2e.ts tests/stress/tabs-component-strip.e2e.ts tests/stress/source-find-scroll.e2e.ts tests/stress/harness-app-warmth.e2e.ts tests/stress/qa-canary-authoring-both-modes.e2e.ts tests/stress/qa-canary-live-typing.e2e.ts tests/stress/keystroke-cadence-danger-space.e2e.ts tests/stress/jsx-unregistered-backspace-delete.e2e.ts tests/stress/jsx-unregistered-ime-concurrent.e2e.ts tests/stress/jsx-backspace-delete.e2e.ts",
"test:e2e": "playwright test tests/stress/ux-interactions.e2e.ts tests/stress/file-tree-create.e2e.ts tests/stress/file-tree-drag-to-root.e2e.ts tests/stress/file-tree-deselect-to-root.e2e.ts tests/stress/file-tree-compact-folders.e2e.ts tests/stress/create-then-rename-editable.e2e.ts tests/stress/find-replace.e2e.ts tests/stress/editor-tabs.e2e.ts tests/stress/crdt-stress.e2e.ts tests/stress/slash-command.e2e.ts tests/stress/paste-fidelity.e2e.ts tests/stress/fr-7a-disconnect-source-mode.e2e.ts tests/stress/docs-open.e2e.ts tests/stress/frozen-table-headers.e2e.ts tests/stress/asset-embed.e2e.ts tests/stress/asset-embed-advanced.e2e.ts tests/stress/asset-click-dispatch.e2e.ts tests/stress/handoff.e2e.ts tests/stress/multi-agent-presence.e2e.ts tests/stress/editor-mode-persistence.e2e.ts tests/stress/sidebar-search-pill.e2e.ts tests/stress/command-palette-semantic.e2e.ts tests/stress/agent-activity-panel.e2e.ts tests/stress/drop-pipeline-auto-open.e2e.ts tests/stress/command-palette-flicker.e2e.ts tests/stress/cm6-list-hanging-indent.e2e.ts tests/stress/ng7-rapid-nav-coherence.e2e.ts tests/stress/reveal-on-activate.e2e.ts tests/stress/selection-indicator.e2e.ts tests/stress/new-file-cross-doc-bleed.e2e.ts tests/stress/editor-mode-flip-cross-doc-bleed.e2e.ts tests/stress/editor-area-viewport-resize.e2e.ts tests/stress/qa-sidebar-responsive.e2e.ts tests/stress/prd-6955-reassertion-repro.e2e.ts tests/stress/prd-6955-reassertion-wedge.e2e.ts tests/stress/prd-6914-repro.e2e.ts tests/stress/showall-lazy-tree.e2e.ts tests/stress/tabs-component-strip.e2e.ts tests/stress/source-find-scroll.e2e.ts tests/stress/harness-app-warmth.e2e.ts tests/stress/qa-canary-authoring-both-modes.e2e.ts tests/stress/qa-canary-live-typing.e2e.ts tests/stress/keystroke-cadence-danger-space.e2e.ts tests/stress/jsx-unregistered-backspace-delete.e2e.ts tests/stress/jsx-unregistered-ime-concurrent.e2e.ts tests/stress/jsx-backspace-delete.e2e.ts tests/stress/link-authoring-bytes.e2e.ts tests/stress/link-authoring-apex.e2e.ts",
"test:e2e:install-browsers": "playwright install chromium webkit firefox",
"test:visual": "playwright test --config playwright.visual.config.ts",
"test:visual:update": "playwright test --config playwright.visual.config.ts --update-snapshots",
Expand Down Expand Up @@ -180,6 +180,7 @@
"@testing-library/jest-dom": "^6",
"@testing-library/react": "^16",
"@testing-library/user-event": "^14",
"@tiptap/starter-kit": "^3.22.3",
"@types/jsdom": "^28",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
Expand Down
17 changes: 10 additions & 7 deletions packages/app/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,19 @@ import { defineConfig } from '@playwright/test';
*/

/**
* Single-browser (Chromium) — all E2E tests use programmatic clipboard
* Single-browser (Chromium). Most E2E tests use programmatic clipboard
* injection via `dispatchEvent(new ClipboardEvent(...))`, not real browser
* clipboard APIs. Cross-browser clipboard differences (Safari user-activation
* rules, Firefox async clipboard restrictions) are not exercised because the
* tests bypass the native clipboard permission model entirely. Running 3×
* clipboard APIs, so cross-browser clipboard differences (Safari
* user-activation rules, Firefox async clipboard restrictions) aren't
* exercised. The one exception is the FR8 clipboard-denial case in
* link-authoring-apex.e2e.ts, which runs a fresh context with clipboard-read
* withheld to assert the real permission gate degrades silently — it needs no
* cross-browser matrix either (the denial path is host-agnostic). Running 3×
* browsers adds ~10 minutes of CI time with zero additional coverage.
*
* If future tests exercise REAL browser clipboard (e.g., `page.keyboard.press
* ('Meta+V')` with system clipboard content), add per-file project scoping
* for those tests only — not a global 3× multiplier.
* If future tests exercise REAL browser clipboard reads/writes that DO vary by
* engine, add per-file project scoping for those tests only — not a global 3×
* multiplier.
*/
const isCI = !!process.env.CI;

Expand Down
24 changes: 6 additions & 18 deletions packages/app/src/editor/bubble-menu/BubbleMenuBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,15 @@ import { useEditorState } from '@tiptap/react';
import { BubbleMenu } from '@tiptap/react/menus';
import { useRef, useState } from 'react';
import { Separator } from '@/components/ui/separator';
import { getFindReplaceState } from '../find-replace/tiptap-find-replace-extension';
import { BlockTypeSelector } from './BlockTypeSelector';
import { shouldShowBubbleMenu } from './bubble-menu-state';
import { EditWithAiBubbleButton } from './EditWithAiBubbleButton';
import { FileBubbleButtons, isFileNodeSelected } from './FileBubbleButtons';
import { FootnoteBubbleButton } from './FootnoteBubbleButton';
import { ImageAlignButtons, isImageNodeSelected } from './ImageAlignButtons';
import { InlineFormatButtons } from './InlineFormatButtons';
import { LinkEditPopover } from './LinkEditPopover';

function shouldShowBubbleMenu({ editor }: { editor: Editor }): boolean {
if (getFindReplaceState(editor.state).query) return false;
if (editor.isActive('codeBlock')) return false;
// Image / File NodeSelection — show the menu so the per-type buttons
// (`ImageAlignButtons` / `FileBubbleButtons`) are reachable even though
// `textBetween` is empty across a leaf atom. Bypasses the text-bearing-
// selection guards below.
if (isImageNodeSelected(editor)) return true;
if (isFileNodeSelected(editor)) return true;
if (editor.state.selection.empty) return false;
const { from, to } = editor.state.selection;
const text = editor.state.doc.textBetween(from, to, ' ');
if (!text.trim()) return false;
return true;
}

export function BubbleMenuBar({
editor,
shortcutEnabled = true,
Expand Down Expand Up @@ -124,7 +108,11 @@ export function BubbleMenuBar({
<Separator orientation="vertical" className="mx-0.5 h-5 data-vertical:self-center" />
<InlineFormatButtons key={tooltipKey} editor={editor} />
<Separator orientation="vertical" className="mx-0.5 h-5 data-vertical:self-center" />
<LinkEditPopover key={`${tooltipKey}-link`} editor={editor} />
<LinkEditPopover
key={`${tooltipKey}-link`}
editor={editor}
shortcutEnabled={shortcutEnabled}
/>
<FootnoteBubbleButton key={`${tooltipKey}-footnote`} editor={editor} />
<EditWithAiBubbleButton
key={`${tooltipKey}-edit-ai`}
Expand Down
Loading