@plannotator/ui 0.55.0 → 0.56.0
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/HANDOFF.md +43 -1
- package/README.md +81 -4
- package/components/AnnotationPanel.tsx +2 -1
- package/components/AttachmentsButton.tsx +1 -1
- package/components/CodeFilePopout.tsx +6 -4
- package/components/ComposerTokenTextarea.tsx +43 -2
- package/components/DocBadges.tsx +47 -35
- package/components/ImageZoomStage.tsx +32 -4
- package/components/Settings.tsx +2 -1
- package/components/TableOfContents.tsx +2 -1
- package/components/Viewer.tsx +43 -17
- package/components/chat/ChatComposer.tsx +192 -0
- package/components/chat/ChatFrame.tsx +524 -0
- package/components/chat/ChatTranscript.tsx +422 -0
- package/components/chat/chatMarkdown.ts +436 -0
- package/components/chat/chatRecipes.tsx +39 -0
- package/components/chat/chatWindowStore.ts +196 -0
- package/components/chat/index.ts +72 -0
- package/components/diagram/DiagramCanvas.tsx +7 -0
- package/components/diagram/DiagramComposer.tsx +15 -3
- package/components/diagram/DiagramViewer.tsx +1 -0
- package/components/docBadgesWidth.ts +29 -0
- package/components/imageZoom.ts +10 -0
- package/components/inbox-thread/FileName.tsx +118 -0
- package/components/inbox-thread/GuideCard.tsx +39 -0
- package/components/inbox-thread/ReaskNote.tsx +25 -0
- package/components/inbox-thread/SkillsLine.tsx +40 -0
- package/components/inbox-thread/Tip.tsx +105 -0
- package/components/inbox-thread/icons.tsx +262 -0
- package/components/inbox-thread/reask.ts +222 -0
- package/hooks/useActiveSection.ts +46 -63
- package/hooks/useAnnotationHighlighter.ts +40 -1
- package/hooks/useFileBrowser.ts +4 -3
- package/inbox-thread.css +4542 -0
- package/package.json +10 -3
- package/styles.css +1 -1
- package/theme.css +1 -0
- package/themes/tokyo-night-moon.css +34 -0
- package/utils/aiChatFormat.ts +32 -11
- package/utils/headingScrollSpy.ts +19 -6
- package/utils/syntaxTheme.ts +1 -0
- package/utils/themeRegistry.ts +11 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The window's icons: Heroicons outline paths (the set Plannotator draws
|
|
3
|
+
* with), as the design of record uses them, and the agent marks from
|
|
4
|
+
* @plannotator/ui.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type React from 'react';
|
|
8
|
+
import { ClaudeIcon, CodexIcon, CursorIcon, OpenCodeIcon, PiIcon } from '../icons/AgentIcons';
|
|
9
|
+
import type { InboxAuthor } from '@plannotator/core/inbox-types';
|
|
10
|
+
|
|
11
|
+
const PATHS = {
|
|
12
|
+
inbox:
|
|
13
|
+
'M2.25 13.5h3.86a2.25 2.25 0 0 1 2.012 1.244l.256.512a2.25 2.25 0 0 0 2.013 1.244h3.218a2.25 2.25 0 0 0 2.013-1.244l.256-.512a2.25 2.25 0 0 1 2.013-1.244h3.859m-19.5.338V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18v-4.162c0-.224-.034-.447-.1-.661L19.24 5.338a2.25 2.25 0 0 0-2.15-1.588H6.911a2.25 2.25 0 0 0-2.15 1.588L2.35 13.177a2.25 2.25 0 0 0-.1.661Z',
|
|
14
|
+
settings:
|
|
15
|
+
'M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75',
|
|
16
|
+
// Light and dark (Heroicons outline): sun, moon, and computer-desktop for "Match system".
|
|
17
|
+
sun: 'M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z',
|
|
18
|
+
moon: 'M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z',
|
|
19
|
+
system:
|
|
20
|
+
'M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25',
|
|
21
|
+
chevR: 'm8.25 4.5 7.5 7.5-7.5 7.5',
|
|
22
|
+
// Settings → Prompts (Heroicons outline chat-bubble-bottom-center-text).
|
|
23
|
+
prompt:
|
|
24
|
+
'M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 0 1 .865-.501 48.172 48.172 0 0 0 3.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z',
|
|
25
|
+
chevD: 'm19.5 8.25-7.5 7.5-7.5-7.5',
|
|
26
|
+
chevU: 'm4.5 15.75 7.5-7.5 7.5 7.5',
|
|
27
|
+
clip: 'm18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13',
|
|
28
|
+
doc: 'M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z',
|
|
29
|
+
x: 'M6 18 18 6M6 6l12 12',
|
|
30
|
+
mic: 'M12 18.75a6 6 0 0 0 6-6v-1.5m-6 7.5a6 6 0 0 1-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 0 1-3-3V4.5a3 3 0 1 1 6 0v8.25a3 3 0 0 1-3 3Z',
|
|
31
|
+
stop: 'M5.25 7.5A2.25 2.25 0 0 1 7.5 5.25h9a2.25 2.25 0 0 1 2.25 2.25v9a2.25 2.25 0 0 1-2.25 2.25h-9a2.25 2.25 0 0 1-2.25-2.25v-9Z',
|
|
32
|
+
archive:
|
|
33
|
+
'm20.25 7.5-.625 10.632a2.25 2.25 0 0 1-2.247 2.118H6.622a2.25 2.25 0 0 1-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125Z',
|
|
34
|
+
reply: 'M9 15 3 9m0 0 6-6M3 9h12a6 6 0 0 1 0 12h-3',
|
|
35
|
+
compose:
|
|
36
|
+
'm16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10',
|
|
37
|
+
send: 'M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5',
|
|
38
|
+
code: 'M17.25 6.75 22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3-4.5 16.5',
|
|
39
|
+
diagram:
|
|
40
|
+
'M2.25 7.125C2.25 6.504 2.754 6 3.375 6h6c.621 0 1.125.504 1.125 1.125v3.75c0 .621-.504 1.125-1.125 1.125h-6a1.125 1.125 0 0 1-1.125-1.125v-3.75ZM14.25 8.625c0-.621.504-1.125 1.125-1.125h5.25c.621 0 1.125.504 1.125 1.125v8.25c0 .621-.504 1.125-1.125 1.125h-5.25a1.125 1.125 0 0 1-1.125-1.125v-8.25ZM3.75 16.125c0-.621.504-1.125 1.125-1.125h5.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-5.25a1.125 1.125 0 0 1-1.125-1.125v-2.25Z',
|
|
41
|
+
restart:
|
|
42
|
+
'M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99',
|
|
43
|
+
dots: 'M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z',
|
|
44
|
+
// The Artifacts mark: Workspaces' ArtifactShapeTrioIcon (apps/web/src/components/icons/ArtifactShapeTrioIcon.tsx,
|
|
45
|
+
// "The Artifacts navigation mark", 2026-08-19): a circle, a rounded square and a triangle, its geometry kept
|
|
46
|
+
// as one path and drawn at the Inbox's stroke (approval "inbox-thread-artifacts").
|
|
47
|
+
artifacts:
|
|
48
|
+
'M15.625 5.5a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0ZM5.5 12H9a1.75 1.75 0 0 1 1.75 1.75v3.5A1.75 1.75 0 0 1 9 19H5.5a1.75 1.75 0 0 1-1.75-1.75v-3.5A1.75 1.75 0 0 1 5.5 12ZM17 11.75l4 7h-8Z',
|
|
49
|
+
book: 'M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25',
|
|
50
|
+
bell: 'M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0',
|
|
51
|
+
question: 'M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z',
|
|
52
|
+
info: 'm11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z',
|
|
53
|
+
check: 'm4.5 12.75 6 6 9-13.5',
|
|
54
|
+
// Heroicons outline square-2-stack: Open in's Copy path.
|
|
55
|
+
// The thread row's menu: Copy link, Copy thread ID (Heroicons link and hashtag).
|
|
56
|
+
link: 'M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244',
|
|
57
|
+
hash: 'M5.25 8.25h15m-16.5 7.5h15m-1.8-13.5-3.9 19.5m-2.1-19.5-3.9 19.5',
|
|
58
|
+
copy: 'M16.5 8.25V6a2.25 2.25 0 0 0-2.25-2.25H6A2.25 2.25 0 0 0 3.75 6v8.25A2.25 2.25 0 0 0 6 16.5h2.25m8.25-8.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-7.5A2.25 2.25 0 0 1 8.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 0 0-2.25 2.25v6',
|
|
59
|
+
clock: 'M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z',
|
|
60
|
+
search: 'm21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z',
|
|
61
|
+
phone: 'M10.5 1.5H8.25A2.25 2.25 0 0 0 6 3.75v16.5a2.25 2.25 0 0 0 2.25 2.25h7.5A2.25 2.25 0 0 0 18 20.25V3.75a2.25 2.25 0 0 0-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3',
|
|
62
|
+
folder:
|
|
63
|
+
'M2.25 12.75V12A2.25 2.25 0 0 1 4.5 9.75h15A2.25 2.25 0 0 1 21.75 12v.75m-8.69-6.44-2.12-2.12a1.5 1.5 0 0 0-1.061-.44H4.5A2.25 2.25 0 0 0 2.25 6v12a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9a2.25 2.25 0 0 0-2.25-2.25h-5.379a1.5 1.5 0 0 1-1.06-.44Z',
|
|
64
|
+
// The Projects header in the sidebar while the section is open (Heroicons outline folder-open); closed it is `folder`.
|
|
65
|
+
folderOpen:
|
|
66
|
+
'M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 0 0-1.883 2.542l.857 6a2.25 2.25 0 0 0 2.227 1.932H19.05a2.25 2.25 0 0 0 2.227-1.932l.857-6a2.25 2.25 0 0 0-1.883-2.542m-16.5 0V6A2.25 2.25 0 0 1 6 3.75h3.879a1.5 1.5 0 0 1 1.06.44l2.122 2.12a1.5 1.5 0 0 0 1.06.44H18A2.25 2.25 0 0 1 20.25 9v.776',
|
|
67
|
+
back: 'M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18',
|
|
68
|
+
// The GitHub mark as an outline (the What's new bar's release link).
|
|
69
|
+
github: 'M9 19c-4.3 1.4-4.3-2.5-6-3m12 5v-3.5c0-1 .1-1.4-.5-2 2.8-.3 5.5-1.4 5.5-6a4.6 4.6 0 0 0-1.3-3.2 4.2 4.2 0 0 0-.1-3.2s-1.1-.3-3.5 1.3a12.3 12.3 0 0 0-6.2 0C6.5 2.8 5.4 3.1 5.4 3.1a4.2 4.2 0 0 0-.1 3.2A4.6 4.6 0 0 0 4 9.5c0 4.6 2.7 5.7 5.5 6-.6.6-.6 1.2-.5 2V21',
|
|
70
|
+
external: 'M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25',
|
|
71
|
+
comment:
|
|
72
|
+
'M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 0 1 .865-.501 48.172 48.172 0 0 0 3.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z',
|
|
73
|
+
expand:
|
|
74
|
+
'M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15',
|
|
75
|
+
// Expand all / Fold all as chevrons (no thread header glyph looks like maximize), and the thread's full screen: a
|
|
76
|
+
// window with two narrow columns at the left and a chevron pushing them away (the sidebar and the list it hides).
|
|
77
|
+
unfold: 'M8.25 15 12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9',
|
|
78
|
+
fold: 'M15.75 19.5 12 15.75 8.25 19.5m7.5-15L12 8.25 8.25 4.5',
|
|
79
|
+
panelsHide: 'M4.125 4.5h15.75c.621 0 1.125.504 1.125 1.125v12.75c0 .621-.504 1.125-1.125 1.125H4.125A1.125 1.125 0 0 1 3 18.375V5.625C3 5.004 3.504 4.5 4.125 4.5ZM7.5 4.5v15M11.25 4.5v15m6-10.5-3 3 3 3',
|
|
80
|
+
// The two views (fileView.ts): a window split by its divider for Side by side, frame corners for Full screen.
|
|
81
|
+
sideBySide: 'M9.75 4.5v15M4.125 19.5h15.75c.621 0 1.125-.504 1.125-1.125V5.625c0-.621-.504-1.125-1.125-1.125H4.125C3.504 4.5 3 5.004 3 5.625v12.75c0 .621.504 1.125 1.125 1.125Z',
|
|
82
|
+
// An open document's Contents list (ContentsPanel.tsx; Heroicons outline list-bullet).
|
|
83
|
+
contents:
|
|
84
|
+
'M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0ZM3.75 12h.007v.008H3.75V12Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm-.375 5.25h.007v.008H3.75v-.008Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z',
|
|
85
|
+
frame: 'M3.75 8.25v-3a1.5 1.5 0 0 1 1.5-1.5h3m7.5 0h3a1.5 1.5 0 0 1 1.5 1.5v3m0 7.5v3a1.5 1.5 0 0 1-1.5 1.5h-3m-7.5 0h-3a1.5 1.5 0 0 1-1.5-1.5v-3',
|
|
86
|
+
collapse:
|
|
87
|
+
'M9 9V4.5M9 9H4.5M9 9 3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5 5.25 5.25',
|
|
88
|
+
download: 'M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3',
|
|
89
|
+
photo:
|
|
90
|
+
'm2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z',
|
|
91
|
+
pen: 'm16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125',
|
|
92
|
+
cube: 'm21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9',
|
|
93
|
+
chevL: 'M15.75 19.5 8.25 12l7.5-7.5',
|
|
94
|
+
// Full screen's toolbar (approval "inbox-one-fullscreen"): » Hide tools (Heroicons chevron-double-right) and the pin that keeps it out (Lucide's pin, as the record draws it).
|
|
95
|
+
chevsR: 'm5.25 4.5 7.5 7.5-7.5 7.5m6-15 7.5 7.5-7.5 7.5',
|
|
96
|
+
// The full-screen composer (approval "inbox-fullscreen-composer"): its minimize, the same double chevron turned toward the foot, and its pill's way back.
|
|
97
|
+
chevsD: 'm4.5 5.25 7.5 7.5 7.5-7.5m-15 6 7.5 7.5 7.5-7.5',
|
|
98
|
+
chevsU: 'm4.5 18.75 7.5-7.5 7.5 7.5m-15-6 7.5-7.5 7.5 7.5',
|
|
99
|
+
pin: 'M12 17v5M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z',
|
|
100
|
+
// Replaced questions (Heroicons outline): arrow-turn-down-right, warning, arrow-down.
|
|
101
|
+
turnDown: 'm16.49 12 3.75 3.75m0 0-3.75 3.75m3.75-3.75H3.74V4.499',
|
|
102
|
+
warn: 'M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z',
|
|
103
|
+
arrowDown: 'M19.5 13.5 12 21m0 0-7.5-7.5M12 21V3',
|
|
104
|
+
// Pinned threads (approval "inbox-pinned-threads"): Unpin is Lucide's pin-off, Move up Heroicons' arrow-up.
|
|
105
|
+
pinOff: 'M12 17v5M15 9.34V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H7.89M2 2l20 20M9 9v1.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h11',
|
|
106
|
+
arrowUp: 'M4.5 10.5 12 3m0 0 7.5 7.5M12 3v18',
|
|
107
|
+
// The Board's "You: …" link to the thread (Heroicons outline arrow-up-right).
|
|
108
|
+
arrowUpRight: 'm4.5 19.5 15-15m0 0H8.25m11.25 0v11.25',
|
|
109
|
+
// The question sound (Heroicons outline): play, and cursor-arrow-rays for "click once in this tab".
|
|
110
|
+
film: 'M6 5.25h12a2.25 2.25 0 0 1 2.25 2.25v9a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25v-9A2.25 2.25 0 0 1 6 5.25ZM7.5 5.25v13.5M16.5 5.25v13.5M3.75 9.75H7.5M3.75 14.25H7.5M16.5 9.75h3.75M16.5 14.25h3.75',
|
|
111
|
+
/** The onboarding tour's tag (Heroicons outline "map"). */
|
|
112
|
+
map: 'M9 6.75V15m6-6v8.25m.503 3.498 4.875-2.437c.381-.19.622-.58.622-1.006V4.82c0-.836-.88-1.38-1.628-1.006l-3.869 1.934c-.317.159-.69.159-1.006 0L9.503 3.252a1.125 1.125 0 0 0-1.006 0L3.622 5.689C3.24 5.88 3 6.27 3 6.695V19.18c0 .836.88 1.38 1.628 1.006l3.869-1.934c.317-.159.69-.159 1.006 0l4.994 2.497c.317.158.69.158 1.006 0Z',
|
|
113
|
+
/** A replay of a real thread (Heroicons outline "arrow-path"). */
|
|
114
|
+
replay: 'M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99',
|
|
115
|
+
/** The tour's Fast-forward (Heroicons outline "forward"): skip to the end of a step. */
|
|
116
|
+
fastForward: 'M3 8.689c0-.864.933-1.406 1.683-.977l7.108 4.061a1.125 1.125 0 0 1 0 1.954l-7.108 4.061A1.125 1.125 0 0 1 3 16.811V8.69ZM12.75 8.689c0-.864.933-1.406 1.683-.977l7.108 4.061a1.125 1.125 0 0 1 0 1.954l-7.108 4.061a1.125 1.125 0 0 1-1.683-.977V8.69Z',
|
|
117
|
+
play: 'M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 0 1 0 1.972l-11.54 6.347a1.125 1.125 0 0 1-1.667-.986V5.653Z',
|
|
118
|
+
clickHere:
|
|
119
|
+
'M15.042 21.672 13.684 16.6m0 0-2.51 2.225.569-9.47 5.227 7.917-3.286-.672ZM12 2.25V4.5m5.834.166-1.591 1.591M20.25 10.5H18M7.757 14.743l-1.59 1.59M6 10.5H3.75m4.007-4.243-1.59-1.59',
|
|
120
|
+
// Mark read (Heroicons outline check-circle), and a Send's skip (arrow-uturn-right).
|
|
121
|
+
checkCircle: 'M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z',
|
|
122
|
+
// Mark unread (Heroicons outline envelope).
|
|
123
|
+
envelope:
|
|
124
|
+
'M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75',
|
|
125
|
+
// The "Sound off" toast (Heroicons outline speaker-x-mark).
|
|
126
|
+
mute: 'M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z',
|
|
127
|
+
forward: 'm15 15 6-6m0 0-6-6m6 6H9a6 6 0 0 0 0 12h3',
|
|
128
|
+
// Folded messages (Heroicons outline rectangle-stack): the "N earlier messages" fold.
|
|
129
|
+
stack:
|
|
130
|
+
'M6 6.878V6a2.25 2.25 0 0 1 2.25-2.25h7.5A2.25 2.25 0 0 1 18 6v.878m-12 0c.235-.083.487-.128.75-.128h10.5c.263 0 .515.045.75.128m-12 0A2.25 2.25 0 0 0 4.5 9v.878m13.5-3A2.25 2.25 0 0 1 19.5 9v.878m0 0a2.246 2.246 0 0 0-.75-.128H5.25c-.263 0-.515.045-.75.128m15 0A2.25 2.25 0 0 1 21 12v6a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 18v-6c0-.98.626-1.813 1.5-2.122',
|
|
131
|
+
// Settings' sections and Pair a phone (Heroicons outline): cpu-chip, circle-stack, wifi, globe-alt,
|
|
132
|
+
// lock-closed, arrow-path, x-circle, exclamation-circle, minus-circle.
|
|
133
|
+
chip: 'M8.25 3v1.5M4.5 8.25H3m18 0h-1.5M4.5 12H3m18 0h-1.5m-15 3.75H3m18 0h-1.5M8.25 19.5V21M12 3v1.5m0 15V21m3.75-18v1.5m0 15V21m-9-1.5h10.5a2.25 2.25 0 0 0 2.25-2.25V6.75a2.25 2.25 0 0 0-2.25-2.25H6.75A2.25 2.25 0 0 0 4.5 6.75v10.5a2.25 2.25 0 0 0 2.25 2.25Zm.75-12h9v9h-9v-9Z',
|
|
134
|
+
disk: 'M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125',
|
|
135
|
+
wifi: 'M8.288 15.038a5.25 5.25 0 0 1 7.424 0M5.106 11.856c3.807-3.808 9.98-3.808 13.788 0M1.924 8.674c5.565-5.565 14.587-5.565 20.152 0M12.53 18.22l-.53.53-.53-.53a.75.75 0 0 1 1.06 0Z',
|
|
136
|
+
globe:
|
|
137
|
+
'M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418',
|
|
138
|
+
lock: 'M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z',
|
|
139
|
+
eyeOff: 'M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88',
|
|
140
|
+
refresh: 'M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99',
|
|
141
|
+
xCircle: 'm9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z',
|
|
142
|
+
excl: 'M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z',
|
|
143
|
+
minus: 'M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z',
|
|
144
|
+
// An action item (approval "inbox-next-actions"): a ticked box, and the eye of "is watching".
|
|
145
|
+
todo: 'M9 12.75 11.25 15 15 9.75M6.75 3.75h10.5a3 3 0 0 1 3 3v10.5a3 3 0 0 1-3 3H6.75a3 3 0 0 1-3-3V6.75a3 3 0 0 1 3-3Z',
|
|
146
|
+
// A list row's open action items (approval "inbox-row-action-icon", A2): the same box with its tick taken out; and
|
|
147
|
+
// the row's open questions (Q3): Lucide's message-circle-question-mark (ISC), its three paths as one.
|
|
148
|
+
todoOpen: 'M6.75 3.75h10.5a3 3 0 0 1 3 3v10.5a3 3 0 0 1-3 3H6.75a3 3 0 0 1-3-3V6.75a3 3 0 0 1 3-3Z',
|
|
149
|
+
questionBubble: 'M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3M12 17h.01',
|
|
150
|
+
eye: 'M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178ZM15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z',
|
|
151
|
+
terminal: 'm6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25Z',
|
|
152
|
+
// The Board (Heroicons outline view-columns), New card (plus), a card the person made (user), Icebox (a snowflake, as the mock: Lucide snowflake).
|
|
153
|
+
board: 'M9 4.5v15m6-15v15m-10.875 0h15.75c.621 0 1.125-.504 1.125-1.125V5.625c0-.621-.504-1.125-1.125-1.125H4.125C3.504 4.5 3 5.004 3 5.625v12.75c0 .621.504 1.125 1.125 1.125Z',
|
|
154
|
+
plus: 'M12 4.5v15m7.5-7.5h-15',
|
|
155
|
+
user: 'M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z',
|
|
156
|
+
snowflake: 'm10 20-1.25-2.5L6 18M10 4 8.75 6.5 6 6m8 14 1.25-2.5L18 18M14 4l1.25 2.5L18 6m-1 15-3-6h-4m7-12-3 6 1.5 3M2 12h6.5L10 9m10 1-1.5 2 1.5 2m2-2h-6.5L14 15M4 10l1.5 2L4 14m3 7 3-6-1.5-3M7 3l3 6h4',
|
|
157
|
+
// The shortcut map's button beside light and dark.
|
|
158
|
+
keyboard: 'M3.75 6.75h16.5a1.5 1.5 0 0 1 1.5 1.5v7.5a1.5 1.5 0 0 1-1.5 1.5H3.75a1.5 1.5 0 0 1-1.5-1.5v-7.5a1.5 1.5 0 0 1 1.5-1.5ZM6.5 10.25h.01m3 0h.01m3 0h.01m3 0h.01m3 0h.01M8.25 13.75h7.5',
|
|
159
|
+
// Settings → Capabilities (Heroicons outline puzzle-piece).
|
|
160
|
+
puzzle:
|
|
161
|
+
'M14.25 6.087c0-.355.186-.676.401-.959.221-.29.349-.634.349-1.003 0-1.036-1.007-1.875-2.25-1.875s-2.25.84-2.25 1.875c0 .369.128.713.349 1.003.215.283.401.604.401.959v0a.64.64 0 0 1-.657.643 48.39 48.39 0 0 1-4.163-.3c.186 1.613.293 3.25.315 4.907a.656.656 0 0 1-.658.663v0c-.355 0-.676-.186-.959-.401a1.647 1.647 0 0 0-1.003-.349c-1.036 0-1.875 1.007-1.875 2.25s.84 2.25 1.875 2.25c.369 0 .713-.128 1.003-.349.283-.215.604-.401.959-.401v0c.31 0 .555.26.532.57a48.039 48.039 0 0 1-.642 5.056c1.518.19 3.058.309 4.616.354a.64.64 0 0 0 .657-.643v0c0-.355-.186-.676-.401-.959a1.647 1.647 0 0 1-.349-1.003c0-1.035 1.008-1.875 2.25-1.875 1.243 0 2.25.84 2.25 1.875 0 .369-.128.713-.349 1.003-.215.283-.4.604-.4.959v0c0 .333.277.599.61.58a48.1 48.1 0 0 0 5.427-.63 48.05 48.05 0 0 0 .582-4.717.532.532 0 0 0-.533-.57v0c-.355 0-.676.186-.959.401-.29.221-.634.349-1.003.349-1.035 0-1.875-1.007-1.875-2.25s.84-2.25 1.875-2.25c.37 0 .713.128 1.003.349.283.215.604.401.96.401v0a.656.656 0 0 0 .658-.663 48.422 48.422 0 0 0-.37-5.36c-1.886.342-3.81.574-5.766.689a.578.578 0 0 1-.61-.58v0Z',
|
|
162
|
+
// Drafts' delete (Heroicons outline trash).
|
|
163
|
+
trash:
|
|
164
|
+
'm14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0',
|
|
165
|
+
} as const;
|
|
166
|
+
|
|
167
|
+
export type IconName = keyof typeof PATHS;
|
|
168
|
+
|
|
169
|
+
/** Tailscale's nine dots, in the current colour (the phone path's mark). */
|
|
170
|
+
export function TailscaleMark({ size = 16 }: { size?: number }) {
|
|
171
|
+
const dots: [number, number, number][] = [
|
|
172
|
+
[4, 4, 0.3], [12, 4, 0.3], [20, 4, 0.3],
|
|
173
|
+
[4, 12, 1], [12, 12, 1], [20, 12, 1],
|
|
174
|
+
[4, 20, 0.3], [12, 20, 1], [20, 20, 0.3],
|
|
175
|
+
];
|
|
176
|
+
return (
|
|
177
|
+
<svg className="ib-tsmark" width={size} height={size} viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
|
178
|
+
{dots.map(([x, y, o]) => (
|
|
179
|
+
<circle key={`${x}-${y}`} cx={x} cy={y} r={2.7} fill="currentColor" opacity={o} />
|
|
180
|
+
))}
|
|
181
|
+
</svg>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function Icon({ name, size, className }: { name: IconName; size?: number; className?: string }) {
|
|
186
|
+
const style = size ? ({ width: size, height: size } as React.CSSProperties) : undefined;
|
|
187
|
+
return (
|
|
188
|
+
<svg className={`ib-ic${className ? ` ${className}` : ''}`} viewBox="0 0 24 24" style={style} aria-hidden="true" focusable="false" data-icon={name}>
|
|
189
|
+
<path d={PATHS[name]} />
|
|
190
|
+
</svg>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Monochrome tab glyphs for the harness picker (the record's, in the muted colour). */
|
|
195
|
+
const TAB_GLYPHS: Partial<Record<string, string>> = {
|
|
196
|
+
cursor: 'M12 2.5 20.5 7.25v9.5L12 21.5 3.5 16.75v-9.5Z M12 12l8.5-4.75 M12 12v9.5 M12 12 3.5 7.25',
|
|
197
|
+
vscode: 'M17 3.5 7.5 12l9.5 8.5 3.5-1.7V5.2Z M17 3.5v17 M7.5 12 3.5 9 M7.5 12 3.5 15',
|
|
198
|
+
gemini: 'M12 2.5c.9 5 3.6 8.6 9.5 9.5-5.9.9-8.6 4.5-9.5 9.5-.9-5-3.6-8.6-9.5-9.5 5.9-.9 8.6-4.5 9.5-9.5Z',
|
|
199
|
+
// Qwen Code: its hexagon around a turned triangle, three bands as one pinwheel.
|
|
200
|
+
qwen: 'M12 2.5 20.23 7.25v9.5L12 21.5 3.77 16.75v-9.5Z M8.97 10.25h6.06L12 15.5Z M8.97 10.25 12 2.5 M15.03 10.25l5.2 6.5 M12 15.5l-8.23 1.25',
|
|
201
|
+
zed: 'M4 4h16v16H4Z M8 8.5h8l-8 7h8',
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export function TabGlyph({ harness }: { harness: string }) {
|
|
205
|
+
if (harness === 'codex') return <CodexIcon className="ib-tglyph ib-img" />;
|
|
206
|
+
const d = TAB_GLYPHS[harness];
|
|
207
|
+
if (!d) return null;
|
|
208
|
+
return (
|
|
209
|
+
<svg className="ib-tglyph" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
|
210
|
+
<path d={d} />
|
|
211
|
+
</svg>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** The mark for an agent host: the real brand marks for the hosts ui carries, a neutral glyph otherwise. */
|
|
216
|
+
export function HostMark({ host, big }: { host: string | null | undefined; big?: boolean }) {
|
|
217
|
+
let mark: React.ReactNode;
|
|
218
|
+
switch (host) {
|
|
219
|
+
case 'claude-code':
|
|
220
|
+
case 'claude':
|
|
221
|
+
case 'claude-app':
|
|
222
|
+
mark = <ClaudeIcon />;
|
|
223
|
+
break;
|
|
224
|
+
case 'gemini':
|
|
225
|
+
case 'qwen':
|
|
226
|
+
case 'zed':
|
|
227
|
+
case 'vscode':
|
|
228
|
+
mark = (
|
|
229
|
+
<svg className="ib-glyph" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
|
230
|
+
<path d={TAB_GLYPHS[host]} />
|
|
231
|
+
</svg>
|
|
232
|
+
);
|
|
233
|
+
break;
|
|
234
|
+
case 'pi':
|
|
235
|
+
mark = <PiIcon />;
|
|
236
|
+
break;
|
|
237
|
+
case 'opencode':
|
|
238
|
+
mark = <OpenCodeIcon />;
|
|
239
|
+
break;
|
|
240
|
+
case 'codex':
|
|
241
|
+
mark = <CodexIcon />;
|
|
242
|
+
break;
|
|
243
|
+
case 'cursor':
|
|
244
|
+
mark = <CursorIcon />;
|
|
245
|
+
break;
|
|
246
|
+
case 'grok':
|
|
247
|
+
// Grok Build: a plain letter G, drawn here (no brand asset in the repo).
|
|
248
|
+
mark = (
|
|
249
|
+
<svg className="ib-glyph" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
|
250
|
+
<path d="M17.6 8.4A6.6 6.6 0 1 0 18.6 12H12.6" />
|
|
251
|
+
</svg>
|
|
252
|
+
);
|
|
253
|
+
break;
|
|
254
|
+
default:
|
|
255
|
+
mark = <Icon name="code" />;
|
|
256
|
+
}
|
|
257
|
+
return <span className={`ib-mk${big ? ' ib-big' : ''}`}>{mark}</span>;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function AuthorMark({ author, big }: { author: InboxAuthor | null | undefined; big?: boolean }) {
|
|
261
|
+
return <HostMark host={author?.kind === 'agent' ? author.host : null} big={big} />;
|
|
262
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-ask (approval "inbox-reask", 2026-10-20): the person asks the agent
|
|
3
|
+
* that asked for its questions still open in the thread again, together in
|
|
4
|
+
* one new message, with fresh context. The agent's new message replaces the
|
|
5
|
+
* old cards (`Replaces:` lines) or closes those that no longer apply
|
|
6
|
+
* (`supersedes`); until then the old ones stay open.
|
|
7
|
+
*
|
|
8
|
+
* Pure: which sessions a Re-ask goes to, and the words around it. One note
|
|
9
|
+
* per session (owner's rule 3): a thread where two sessions asked gets two.
|
|
10
|
+
* Picks the person made and has not sent go with the note as answers
|
|
11
|
+
* (owner's rule 2); only the rest are asked again.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { InboxMessageWire, InboxQuestion } from '@plannotator/core/inbox-types';
|
|
15
|
+
import { agentName, plural } from '@plannotator/core/inbox-format';
|
|
16
|
+
|
|
17
|
+
/** One session's part of a Re-ask. */
|
|
18
|
+
export interface ReaskTarget {
|
|
19
|
+
/**
|
|
20
|
+
* `reask`: a note asking the session for its open questions again (its
|
|
21
|
+
* unsent picks go with it). `picks`: the session has nothing open, only
|
|
22
|
+
* picks not sent: a plain reply carries them as answers, so a Re-ask never
|
|
23
|
+
* strands them.
|
|
24
|
+
*/
|
|
25
|
+
kind: 'reask' | 'picks';
|
|
26
|
+
/** The session's latest message with a question still open or picked: the note answers it. */
|
|
27
|
+
message: InboxMessageWire;
|
|
28
|
+
session: string | null;
|
|
29
|
+
host: string | null;
|
|
30
|
+
/** "Claude Code": the agent as the thread names it. */
|
|
31
|
+
name: string;
|
|
32
|
+
/** Open questions with no pick, and open action items: asked again. */
|
|
33
|
+
open: number;
|
|
34
|
+
/** How many of `open` are action items. */
|
|
35
|
+
actions: number;
|
|
36
|
+
/** Picked and not sent: they go with the note (or the plain reply) as answers. */
|
|
37
|
+
picked: number;
|
|
38
|
+
/**
|
|
39
|
+
* For `picks`: the one plain reply that carries them, as a Send does (one
|
|
40
|
+
* reply per session, approval "inbox-explicit-skip"): it answers the
|
|
41
|
+
* session's latest message holding a pick, and names the message of every
|
|
42
|
+
* pick on another one.
|
|
43
|
+
*/
|
|
44
|
+
replies: { message: InboxMessageWire; questions: { message_id?: string; key: string; revision: number }[] }[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const isReaskNote = (m: InboxMessageWire): boolean => m.author.kind === 'person' && (m.reask?.length ?? 0) > 0;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* What a Re-ask can ask about in a message: its questions and its action
|
|
51
|
+
* items (the window keeps those in `actions`; owner, #2059: Re-ask includes
|
|
52
|
+
* open action items). An action item the person marked (Done or Can't do it,
|
|
53
|
+
* inside its Undo window) reads "picked" but waits on nobody: it is left out.
|
|
54
|
+
*/
|
|
55
|
+
const itemsOf = (m: InboxMessageWire): InboxQuestion[] => [...(m.questions ?? []), ...(m.actions ?? [])];
|
|
56
|
+
const isWaiting = (q: InboxQuestion) => q.state === 'open' || (q.state === 'picked' && q.kind !== 'action');
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The sessions a Re-ask would go to now. A session is left out while a note
|
|
60
|
+
* of its waits (rule 2a): no message after it from the session the note went
|
|
61
|
+
* to (the note's `to` when the person sent it to another session because
|
|
62
|
+
* this one ended, else this session), and a question it lists still open.
|
|
63
|
+
* Once that session wrote, or nothing it lists is open, what is left can be
|
|
64
|
+
* asked again. Also left out right after it
|
|
65
|
+
* answered a note (every question it still has open sits in its one message
|
|
66
|
+
* that answered the note), and when nothing of its waits. `live` says which
|
|
67
|
+
* messages still take answers (not closed by an archive).
|
|
68
|
+
*/
|
|
69
|
+
export function reaskTargets(messages: readonly InboxMessageWire[], live: (message: InboxMessageWire) => boolean = () => true): ReaskTarget[] {
|
|
70
|
+
const groups = new Map<string, InboxMessageWire[]>();
|
|
71
|
+
for (const m of messages) {
|
|
72
|
+
if (m.author.kind !== 'agent' || !live(m)) continue;
|
|
73
|
+
// An agent that named no session: each message is its own.
|
|
74
|
+
const key = m.author.session ? `${m.author.host ?? ''}\u0000${m.author.session}` : `\u0001${m.id}`;
|
|
75
|
+
const list = groups.get(key) ?? [];
|
|
76
|
+
list.push(m);
|
|
77
|
+
groups.set(key, list);
|
|
78
|
+
}
|
|
79
|
+
const wroteAfter = (note: InboxMessageWire, by: { host: string | null; session: string | null }) => {
|
|
80
|
+
const at = messages.indexOf(note);
|
|
81
|
+
return messages.slice(at + 1).some((m) => m.author.kind === 'agent' && m.author.session === by.session && (m.author.host ?? '') === (by.host ?? ''));
|
|
82
|
+
};
|
|
83
|
+
// Ids are built from the message that holds each question, never from the
|
|
84
|
+
// question's own `message_id`, so a question record without one (or a stale
|
|
85
|
+
// one) can never point a rule at a message that is not there.
|
|
86
|
+
const openIds = new Set(
|
|
87
|
+
messages.flatMap((m) => (m.author.kind === 'agent' && live(m) ? itemsOf(m).filter((q) => q.state === 'open').map((q) => `${m.id}/${q.key}`) : [])),
|
|
88
|
+
);
|
|
89
|
+
const targets: ReaskTarget[] = [];
|
|
90
|
+
for (const group of groups.values()) {
|
|
91
|
+
const ids = new Set(group.map((m) => m.id));
|
|
92
|
+
const first = group[0];
|
|
93
|
+
if (!first) continue;
|
|
94
|
+
const author = first.author.kind === 'agent' ? first.author : null;
|
|
95
|
+
const own = { host: author?.host ?? null, session: author?.session ?? null };
|
|
96
|
+
// A note to this session (an answer to one of its messages), or one sent to it from an ended session.
|
|
97
|
+
const notes = messages.filter((m) => isReaskNote(m) && ((m.reply_to && ids.has(m.reply_to)) || (own.session && m.to?.session === own.session)));
|
|
98
|
+
// A note waits until its session wrote again, and only while a question it lists is still open (rule 2a).
|
|
99
|
+
const waits = (note: InboxMessageWire) =>
|
|
100
|
+
!wroteAfter(note, note.to && note.to.session !== own.session ? { host: note.to.host, session: note.to.session } : own) &&
|
|
101
|
+
(note.reask ?? []).some((id) => openIds.has(id));
|
|
102
|
+
if (notes.some(waits)) continue;
|
|
103
|
+
// The session's messages that still hold a question waiting on the person
|
|
104
|
+
// (open, or picked and not sent), in thread order. Every rule below reads
|
|
105
|
+
// these messages, not the questions' `message_id`, so the message a rule
|
|
106
|
+
// names is always one of the session's own.
|
|
107
|
+
const holders = group.filter((m) => itemsOf(m).some(isWaiting));
|
|
108
|
+
const target = holders.at(-1);
|
|
109
|
+
if (!target) continue;
|
|
110
|
+
const waiting = holders.flatMap((m) => itemsOf(m).filter(isWaiting));
|
|
111
|
+
const open = waiting.filter((q) => q.state === 'open').length;
|
|
112
|
+
const actions = waiting.filter((q) => q.state === 'open' && q.kind === 'action').length;
|
|
113
|
+
const base = { message: target, session: own.session, host: own.host, name: agentName(first.author), open, actions, picked: waiting.length - open };
|
|
114
|
+
if (open === 0) {
|
|
115
|
+
// Only picks: one plain reply carries them all (owner's rule: picks go, never stranded).
|
|
116
|
+
const questions = holders.flatMap((m) =>
|
|
117
|
+
(m.questions ?? [])
|
|
118
|
+
.filter((q) => q.state === 'picked')
|
|
119
|
+
.map((q) => (m.id === target.id ? { key: q.key, revision: q.revision } : { message_id: m.id, key: q.key, revision: q.revision })),
|
|
120
|
+
);
|
|
121
|
+
targets.push({ ...base, kind: 'picks', replies: questions.length > 0 ? [{ message: target, questions }] : [] });
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
// Just answered a note: everything still waiting is in that one message,
|
|
125
|
+
// and that message replies to the note.
|
|
126
|
+
if (holders.length === 1) {
|
|
127
|
+
const answers = target.reply_to ? messages.find((m) => m.id === target.reply_to) : undefined;
|
|
128
|
+
if (answers && isReaskNote(answers)) continue;
|
|
129
|
+
}
|
|
130
|
+
targets.push({ ...base, kind: 'reask', replies: [] });
|
|
131
|
+
}
|
|
132
|
+
return targets;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Re-ask is offered when a question or action item waits in the thread (open,
|
|
137
|
+
* or a question picked and not sent; owner, 2026-10-23: "re-ask should show
|
|
138
|
+
* for even 1 question back in a thread") and some session has an open one it
|
|
139
|
+
* can be asked for again. `messages`: the thread's live ones.
|
|
140
|
+
*/
|
|
141
|
+
export function reaskOffered(targets: readonly ReaskTarget[], messages?: readonly InboxMessageWire[]): boolean {
|
|
142
|
+
const waiting = messages
|
|
143
|
+
? messages.reduce((n, m) => n + (m.author.kind === 'agent' ? itemsOf(m).filter(isWaiting).length : 0), 0)
|
|
144
|
+
: targets.reduce((n, t) => n + t.open + t.picked, 0);
|
|
145
|
+
return waiting >= 1 && targets.some((t) => t.kind === 'reask' && t.open > 0);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** "Claude Code", "Claude Code and Pi", or "2 sessions" when two share a name. */
|
|
149
|
+
export function reaskWho(targets: readonly ReaskTarget[]): string {
|
|
150
|
+
const names = targets.filter((t) => t.kind === 'reask').map((t) => t.name);
|
|
151
|
+
if (names.length === 1) return names[0]!;
|
|
152
|
+
if (new Set(names).size < names.length) return `${names.length} sessions`;
|
|
153
|
+
return `${names.slice(0, -1).join(', ')} and ${names.at(-1)}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* "the open question", "the 3 open questions", "the open action item", "the 2
|
|
158
|
+
* open questions and 1 action item": what a Re-ask asks about.
|
|
159
|
+
*/
|
|
160
|
+
export function reaskItems(open: number, actions: number): string {
|
|
161
|
+
const questions = open - actions;
|
|
162
|
+
if (actions === 0) return open === 1 ? 'the open question' : `the ${open} open questions`;
|
|
163
|
+
if (questions === 0) return actions === 1 ? 'the open action item' : `the ${actions} open action items`;
|
|
164
|
+
return `the ${plural(questions, 'open question')} and ${plural(actions, 'action item')}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The button's tooltip: what pressing it does. */
|
|
168
|
+
export function reaskTooltip(targets: readonly ReaskTarget[]): string {
|
|
169
|
+
const open = targets.reduce((n, t) => n + t.open, 0);
|
|
170
|
+
const actions = targets.reduce((n, t) => n + (t.actions ?? 0), 0);
|
|
171
|
+
const picks = targets.reduce((n, t) => n + t.picked, 0);
|
|
172
|
+
const several = targets.filter((t) => t.kind === 'reask').length > 1;
|
|
173
|
+
const pickWords = picks > 0 ? ` Your ${plural(picks, 'pick')} ${picks === 1 ? 'is' : 'are'} sent too.` : '';
|
|
174
|
+
// One open question or action item: singular words, nothing to put "together".
|
|
175
|
+
if (open === 1) {
|
|
176
|
+
const item = actions === 1 ? 'the open action item' : 'the open question';
|
|
177
|
+
return `Ask ${reaskWho(targets)} to ask ${item} again, with fresh context. If it no longer applies, it is closed; until then it stays open here.${pickWords}`;
|
|
178
|
+
}
|
|
179
|
+
if (actions > 0) {
|
|
180
|
+
const items = reaskItems(open, actions).replace(/^the /, several ? 'their ' : 'the ');
|
|
181
|
+
return `Ask ${reaskWho(targets)} to ask ${items} again, ${several ? 'each together in one message' : 'together in one message'} with fresh context. Those that no longer apply are closed; until then they stay open here.${pickWords}`;
|
|
182
|
+
}
|
|
183
|
+
const what = `${several ? 'their' : 'the'} ${open} open questions again, ${several ? 'each together in one message' : 'together in one message'} with fresh context`;
|
|
184
|
+
return `Ask ${reaskWho(targets)} to ask ${what}. Questions that no longer apply are closed; until then they stay open here.${pickWords}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The note's line in the thread. `picks`: answers that went with it; `actions`: how many of `asked` are action items. */
|
|
188
|
+
export function reaskNoteSentence(agent: string, asked: number, picks: number, actions = 0): string {
|
|
189
|
+
if (actions > 0) {
|
|
190
|
+
const items = reaskItems(asked, actions);
|
|
191
|
+
const line =
|
|
192
|
+
asked === 1
|
|
193
|
+
? `You asked ${agent} to ask ${items} again. It stays open here until it does.`
|
|
194
|
+
: `You asked ${agent} to ask ${items} again, together in one message. They stay open here until it does.`;
|
|
195
|
+
return picks > 0 ? `${line} Your ${plural(picks, 'pick')} went with it.` : line;
|
|
196
|
+
}
|
|
197
|
+
const head =
|
|
198
|
+
asked === 1
|
|
199
|
+
? `You asked ${agent} to ask the open question again. It stays open here until it does.`
|
|
200
|
+
: `You asked ${agent} to ask the ${asked} open questions again, together in one message. They stay open here until it does.`;
|
|
201
|
+
return picks > 0 ? `${head} Your ${plural(picks, 'pick')} went with it.` : head;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The note a question was last asked again in, while it is still open.
|
|
206
|
+
* `messageId`: the message that holds the question (not the question's own
|
|
207
|
+
* `message_id`, which a record may lack).
|
|
208
|
+
*/
|
|
209
|
+
export function reaskedIn(
|
|
210
|
+
messages: readonly InboxMessageWire[],
|
|
211
|
+
messageId: string,
|
|
212
|
+
question: Pick<InboxQuestion, 'key' | 'state'>,
|
|
213
|
+
): InboxMessageWire | null {
|
|
214
|
+
if (question.state !== 'open') return null;
|
|
215
|
+
const id = `${messageId}/${question.key}`;
|
|
216
|
+
return [...messages].reverse().find((m) => isReaskNote(m) && (m.reask ?? []).includes(id)) ?? null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Who a Re-ask note went to: the agent of the message it answers, else `fallback` (the thread's asker). */
|
|
220
|
+
export function reaskNoteAgent(answered: InboxMessageWire['author'] | undefined, fallback: string): string {
|
|
221
|
+
return answered?.kind === 'agent' ? agentName(answered) : fallback;
|
|
222
|
+
}
|
|
@@ -1,81 +1,64 @@
|
|
|
1
|
-
import { useEffect, useState
|
|
2
|
-
import {
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { CONTENTS_ACTIVATION_OFFSET } from '../utils/headingScrollSpy';
|
|
3
|
+
import { addScrollViewportListener, getScrollViewportRect } from './useScrollViewport';
|
|
3
4
|
|
|
4
|
-
/**
|
|
5
|
-
* Track which heading section is currently visible in the viewport
|
|
6
|
-
* Uses Intersection Observer to detect when headings enter/leave view
|
|
7
|
-
*/
|
|
5
|
+
/** Track the heading at the reading line, even when a scroll skips whole sections. */
|
|
8
6
|
export function useActiveSection(
|
|
9
7
|
containerRef: React.RefObject<HTMLElement | null>,
|
|
10
8
|
headingCount: number,
|
|
11
|
-
/** Optional explicit scroll element
|
|
12
|
-
* containerRef.current and re-runs the effect when it changes — needed
|
|
13
|
-
* for OverlayScrollArea, whose viewport is only available after mount. */
|
|
9
|
+
/** Optional explicit scroll element, available after OverlayScrollArea mounts. */
|
|
14
10
|
scrollElement?: HTMLElement | null,
|
|
15
11
|
) {
|
|
16
12
|
const [activeId, setActiveId] = useState<string | null>(null);
|
|
17
|
-
const observerRef = useRef<IntersectionObserver | null>(null);
|
|
18
13
|
|
|
19
14
|
useEffect(() => {
|
|
20
15
|
const contentContainer = containerRef.current ?? scrollElement;
|
|
21
16
|
const viewport = scrollElement ?? contentContainer;
|
|
22
17
|
if (!contentContainer || !viewport) return;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
intersectingHeadings.set(entry.target, entry.isIntersecting);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// Find the first intersecting heading (topmost in viewport)
|
|
41
|
-
let topHeading: Element | null = null;
|
|
42
|
-
let topPosition = Infinity;
|
|
43
|
-
|
|
44
|
-
for (const [heading, isIntersecting] of intersectingHeadings) {
|
|
45
|
-
if (isIntersecting) {
|
|
46
|
-
const rect = heading.getBoundingClientRect();
|
|
47
|
-
if (rect.top < topPosition) {
|
|
48
|
-
topPosition = rect.top;
|
|
49
|
-
topHeading = heading;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Update active ID
|
|
55
|
-
if (topHeading) {
|
|
56
|
-
const blockId = topHeading.getAttribute('data-block-id');
|
|
57
|
-
if (blockId) {
|
|
58
|
-
setActiveId(blockId);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
root: getScrollViewportIntersectionRoot(viewport),
|
|
64
|
-
rootMargin: '-80px 0px -80% 0px', // Activate when heading is near top
|
|
65
|
-
threshold: [0, 0.1, 0.5, 1.0],
|
|
18
|
+
|
|
19
|
+
const refresh = () => {
|
|
20
|
+
// Read current nodes, not a count-keyed snapshot: another document can
|
|
21
|
+
// replace every heading while keeping the same count and block ids.
|
|
22
|
+
const headings = contentContainer.querySelectorAll<HTMLElement>(
|
|
23
|
+
'[data-block-type="heading"][data-block-id]',
|
|
24
|
+
);
|
|
25
|
+
const readingLine = getScrollViewportRect(viewport).top + CONTENTS_ACTIVATION_OFFSET;
|
|
26
|
+
let current: HTMLElement | null = null;
|
|
27
|
+
for (const heading of headings) {
|
|
28
|
+
if (heading.getClientRects().length === 0) continue;
|
|
29
|
+
current ??= heading;
|
|
30
|
+
if (heading.getBoundingClientRect().top <= readingLine) current = heading;
|
|
31
|
+
else break;
|
|
66
32
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
33
|
+
setActiveId(current?.dataset.blockId ?? null);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
let frame: number | null = null;
|
|
37
|
+
const scheduleRefresh = () => {
|
|
38
|
+
if (frame !== null) return;
|
|
39
|
+
frame = requestAnimationFrame(() => {
|
|
40
|
+
frame = null;
|
|
41
|
+
refresh();
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
refresh();
|
|
45
|
+
const removeScrollListener = addScrollViewportListener(viewport, scheduleRefresh);
|
|
46
|
+
window.addEventListener('resize', scheduleRefresh);
|
|
47
|
+
const resizeObserver = typeof ResizeObserver === 'undefined'
|
|
48
|
+
? null : new ResizeObserver(scheduleRefresh);
|
|
49
|
+
resizeObserver?.observe(contentContainer);
|
|
50
|
+
if (viewport !== contentContainer) resizeObserver?.observe(viewport);
|
|
51
|
+
const mutationObserver = new MutationObserver(scheduleRefresh);
|
|
52
|
+
mutationObserver.observe(contentContainer, { childList: true, characterData: true, subtree: true });
|
|
53
|
+
|
|
75
54
|
return () => {
|
|
76
|
-
|
|
55
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
56
|
+
removeScrollListener();
|
|
57
|
+
window.removeEventListener('resize', scheduleRefresh);
|
|
58
|
+
resizeObserver?.disconnect();
|
|
59
|
+
mutationObserver.disconnect();
|
|
77
60
|
};
|
|
78
61
|
}, [containerRef, headingCount, scrollElement]);
|
|
79
|
-
|
|
62
|
+
|
|
80
63
|
return activeId;
|
|
81
64
|
}
|