mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-25 00:16:29 +02:00
feat(suggested-topics): populate and integrate suggested topics
This commit is contained in:
parent
e9cdd3f6eb
commit
eaab438666
6 changed files with 448 additions and 66 deletions
|
|
@ -262,6 +262,60 @@ const isGraphTabPath = (path: string) => path === GRAPH_TAB_PATH
|
||||||
const isSuggestedTopicsTabPath = (path: string) => path === SUGGESTED_TOPICS_TAB_PATH
|
const isSuggestedTopicsTabPath = (path: string) => path === SUGGESTED_TOPICS_TAB_PATH
|
||||||
const isBaseFilePath = (path: string) => path.endsWith('.base') || path === BASES_DEFAULT_TAB_PATH
|
const isBaseFilePath = (path: string) => path.endsWith('.base') || path === BASES_DEFAULT_TAB_PATH
|
||||||
|
|
||||||
|
const getSuggestedTopicTargetFolder = (category?: string) => {
|
||||||
|
const normalized = category?.trim().toLowerCase()
|
||||||
|
switch (normalized) {
|
||||||
|
case 'people':
|
||||||
|
case 'person':
|
||||||
|
return 'People'
|
||||||
|
case 'organizations':
|
||||||
|
case 'organization':
|
||||||
|
return 'Organizations'
|
||||||
|
case 'projects':
|
||||||
|
case 'project':
|
||||||
|
return 'Projects'
|
||||||
|
case 'meetings':
|
||||||
|
case 'meeting':
|
||||||
|
return 'Meetings'
|
||||||
|
case 'topics':
|
||||||
|
case 'topic':
|
||||||
|
default:
|
||||||
|
return 'Topics'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildSuggestedTopicExplorePrompt = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
category,
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
category?: string
|
||||||
|
}) => {
|
||||||
|
const folder = getSuggestedTopicTargetFolder(category)
|
||||||
|
const categoryLabel = category?.trim() || 'Topics'
|
||||||
|
return [
|
||||||
|
'I am exploring a suggested topic card from the Suggested Topics panel.',
|
||||||
|
'This card may represent a person, organization, topic, or project.',
|
||||||
|
'',
|
||||||
|
'Card context:',
|
||||||
|
`- Title: ${title}`,
|
||||||
|
`- Category: ${categoryLabel}`,
|
||||||
|
`- Description: ${description}`,
|
||||||
|
`- Target folder if we set this up: knowledge/${folder}/`,
|
||||||
|
'',
|
||||||
|
`Please start by telling me that you can set up a tracking note for "${title}" under knowledge/${folder}/.`,
|
||||||
|
'Then briefly explain what that tracking note would monitor or refresh and ask me if you should set it up.',
|
||||||
|
'Do not create or modify anything yet.',
|
||||||
|
'Treat a clear confirmation from me as explicit approval to proceed.',
|
||||||
|
`If I confirm later, load the \`tracks\` skill first, check whether a matching note already exists under knowledge/${folder}/, and update it instead of creating a duplicate.`,
|
||||||
|
`If no matching note exists, create a new note under knowledge/${folder}/ with an appropriate filename.`,
|
||||||
|
'Use a track block in that note rather than only writing static content, and keep any surrounding note scaffolding short and useful.',
|
||||||
|
'Do not ask me to choose a note path unless there is a real ambiguity you cannot resolve from the card.',
|
||||||
|
].join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
const normalizeUsage = (usage?: Partial<LanguageModelUsage> | null): LanguageModelUsage | null => {
|
const normalizeUsage = (usage?: Partial<LanguageModelUsage> | null): LanguageModelUsage | null => {
|
||||||
if (!usage) return null
|
if (!usage) return null
|
||||||
const hasNumbers = Object.values(usage).some((value) => typeof value === 'number')
|
const hasNumbers = Object.values(usage).some((value) => typeof value === 'number')
|
||||||
|
|
@ -585,7 +639,7 @@ function App() {
|
||||||
const [isGraphOpen, setIsGraphOpen] = useState(false)
|
const [isGraphOpen, setIsGraphOpen] = useState(false)
|
||||||
const [isBrowserOpen, setIsBrowserOpen] = useState(false)
|
const [isBrowserOpen, setIsBrowserOpen] = useState(false)
|
||||||
const [isSuggestedTopicsOpen, setIsSuggestedTopicsOpen] = useState(false)
|
const [isSuggestedTopicsOpen, setIsSuggestedTopicsOpen] = useState(false)
|
||||||
const [expandedFrom, setExpandedFrom] = useState<{ path: string | null; graph: boolean } | null>(null)
|
const [expandedFrom, setExpandedFrom] = useState<{ path: string | null; graph: boolean; suggestedTopics: boolean } | null>(null)
|
||||||
const [baseConfigByPath, setBaseConfigByPath] = useState<Record<string, BaseConfig>>({})
|
const [baseConfigByPath, setBaseConfigByPath] = useState<Record<string, BaseConfig>>({})
|
||||||
const [graphData, setGraphData] = useState<{ nodes: GraphNode[]; edges: GraphEdge[] }>({
|
const [graphData, setGraphData] = useState<{ nodes: GraphNode[]; edges: GraphEdge[] }>({
|
||||||
nodes: [],
|
nodes: [],
|
||||||
|
|
@ -2664,15 +2718,16 @@ function App() {
|
||||||
}
|
}
|
||||||
handleNewChat()
|
handleNewChat()
|
||||||
// Left-pane "new chat" should always open full chat view.
|
// Left-pane "new chat" should always open full chat view.
|
||||||
if (selectedPath || isGraphOpen) {
|
if (selectedPath || isGraphOpen || isSuggestedTopicsOpen) {
|
||||||
setExpandedFrom({ path: selectedPath, graph: isGraphOpen })
|
setExpandedFrom({ path: selectedPath, graph: isGraphOpen, suggestedTopics: isSuggestedTopicsOpen })
|
||||||
} else {
|
} else {
|
||||||
setExpandedFrom(null)
|
setExpandedFrom(null)
|
||||||
}
|
}
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
}, [chatTabs, activeChatTabId, handleNewChat, selectedPath, isGraphOpen])
|
setIsSuggestedTopicsOpen(false)
|
||||||
|
}, [chatTabs, activeChatTabId, handleNewChat, selectedPath, isGraphOpen, isSuggestedTopicsOpen])
|
||||||
|
|
||||||
// Sidebar variant: create/switch chat tab without leaving file/graph context.
|
// Sidebar variant: create/switch chat tab without leaving file/graph context.
|
||||||
const handleNewChatTabInSidebar = useCallback(() => {
|
const handleNewChatTabInSidebar = useCallback(() => {
|
||||||
|
|
@ -2766,19 +2821,26 @@ function App() {
|
||||||
|
|
||||||
const handleOpenFullScreenChat = useCallback(() => {
|
const handleOpenFullScreenChat = useCallback(() => {
|
||||||
// Remember where we came from so the close button can return
|
// Remember where we came from so the close button can return
|
||||||
if (selectedPath || isGraphOpen) {
|
if (selectedPath || isGraphOpen || isSuggestedTopicsOpen) {
|
||||||
setExpandedFrom({ path: selectedPath, graph: isGraphOpen })
|
setExpandedFrom({ path: selectedPath, graph: isGraphOpen, suggestedTopics: isSuggestedTopicsOpen })
|
||||||
}
|
}
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
}, [selectedPath, isGraphOpen])
|
setIsSuggestedTopicsOpen(false)
|
||||||
|
}, [selectedPath, isGraphOpen, isSuggestedTopicsOpen])
|
||||||
|
|
||||||
const handleCloseFullScreenChat = useCallback(() => {
|
const handleCloseFullScreenChat = useCallback(() => {
|
||||||
if (expandedFrom) {
|
if (expandedFrom) {
|
||||||
if (expandedFrom.graph) {
|
if (expandedFrom.graph) {
|
||||||
setIsGraphOpen(true)
|
setIsGraphOpen(true)
|
||||||
|
setIsSuggestedTopicsOpen(false)
|
||||||
|
} else if (expandedFrom.suggestedTopics) {
|
||||||
|
setIsGraphOpen(false)
|
||||||
|
setIsSuggestedTopicsOpen(true)
|
||||||
} else if (expandedFrom.path) {
|
} else if (expandedFrom.path) {
|
||||||
|
setIsGraphOpen(false)
|
||||||
|
setIsSuggestedTopicsOpen(false)
|
||||||
setSelectedPath(expandedFrom.path)
|
setSelectedPath(expandedFrom.path)
|
||||||
}
|
}
|
||||||
setExpandedFrom(null)
|
setExpandedFrom(null)
|
||||||
|
|
@ -3179,7 +3241,7 @@ function App() {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Keyboard shortcut: Ctrl+L to toggle main chat view
|
// Keyboard shortcut: Ctrl+L to toggle main chat view
|
||||||
const isFullScreenChat = !selectedPath && !isGraphOpen && !selectedBackgroundTask && !isBrowserOpen
|
const isFullScreenChat = !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !selectedBackgroundTask && !isBrowserOpen
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'l') {
|
if ((e.ctrlKey || e.metaKey) && e.key === 'l') {
|
||||||
|
|
@ -3257,12 +3319,16 @@ function App() {
|
||||||
const handleTabKeyDown = (e: KeyboardEvent) => {
|
const handleTabKeyDown = (e: KeyboardEvent) => {
|
||||||
const mod = e.metaKey || e.ctrlKey
|
const mod = e.metaKey || e.ctrlKey
|
||||||
if (!mod) return
|
if (!mod) return
|
||||||
const rightPaneAvailable = Boolean((selectedPath || isGraphOpen) && isChatSidebarOpen)
|
const rightPaneAvailable = Boolean((selectedPath || isGraphOpen || isSuggestedTopicsOpen) && isChatSidebarOpen)
|
||||||
const targetPane: ShortcutPane = rightPaneAvailable
|
const targetPane: ShortcutPane = rightPaneAvailable
|
||||||
? (isRightPaneMaximized ? 'right' : activeShortcutPane)
|
? (isRightPaneMaximized ? 'right' : activeShortcutPane)
|
||||||
: 'left'
|
: 'left'
|
||||||
const inFileView = targetPane === 'left' && Boolean(selectedPath || isGraphOpen)
|
const inFileView = targetPane === 'left' && Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen)
|
||||||
const selectedKnowledgePath = isGraphOpen ? GRAPH_TAB_PATH : selectedPath
|
const selectedKnowledgePath = isGraphOpen
|
||||||
|
? GRAPH_TAB_PATH
|
||||||
|
: isSuggestedTopicsOpen
|
||||||
|
? SUGGESTED_TOPICS_TAB_PATH
|
||||||
|
: selectedPath
|
||||||
const targetFileTabId = activeFileTabId ?? (
|
const targetFileTabId = activeFileTabId ?? (
|
||||||
selectedKnowledgePath
|
selectedKnowledgePath
|
||||||
? (fileTabs.find((tab) => tab.path === selectedKnowledgePath)?.id ?? null)
|
? (fileTabs.find((tab) => tab.path === selectedKnowledgePath)?.id ?? null)
|
||||||
|
|
@ -3316,7 +3382,7 @@ function App() {
|
||||||
}
|
}
|
||||||
document.addEventListener('keydown', handleTabKeyDown)
|
document.addEventListener('keydown', handleTabKeyDown)
|
||||||
return () => document.removeEventListener('keydown', handleTabKeyDown)
|
return () => document.removeEventListener('keydown', handleTabKeyDown)
|
||||||
}, [selectedPath, isGraphOpen, isChatSidebarOpen, isRightPaneMaximized, activeShortcutPane, chatTabs, fileTabs, activeChatTabId, activeFileTabId, closeChatTab, closeFileTab, switchChatTab, switchFileTab])
|
}, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isChatSidebarOpen, isRightPaneMaximized, activeShortcutPane, chatTabs, fileTabs, activeChatTabId, activeFileTabId, closeChatTab, closeFileTab, switchChatTab, switchFileTab])
|
||||||
|
|
||||||
const toggleExpand = (path: string, kind: 'file' | 'dir') => {
|
const toggleExpand = (path: string, kind: 'file' | 'dir') => {
|
||||||
if (kind === 'file') {
|
if (kind === 'file') {
|
||||||
|
|
@ -3341,7 +3407,7 @@ function App() {
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
if (!selectedPath && !isGraphOpen && !selectedBackgroundTask) {
|
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !selectedBackgroundTask) {
|
||||||
setIsChatSidebarOpen(false)
|
setIsChatSidebarOpen(false)
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
}
|
}
|
||||||
|
|
@ -3463,14 +3529,14 @@ function App() {
|
||||||
},
|
},
|
||||||
openGraph: () => {
|
openGraph: () => {
|
||||||
// From chat-only landing state, open graph directly in full knowledge view.
|
// From chat-only landing state, open graph directly in full knowledge view.
|
||||||
if (!selectedPath && !isGraphOpen && !selectedBackgroundTask) {
|
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !selectedBackgroundTask) {
|
||||||
setIsChatSidebarOpen(false)
|
setIsChatSidebarOpen(false)
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
}
|
}
|
||||||
void navigateToView({ type: 'graph' })
|
void navigateToView({ type: 'graph' })
|
||||||
},
|
},
|
||||||
openBases: () => {
|
openBases: () => {
|
||||||
if (!selectedPath && !isGraphOpen && !selectedBackgroundTask) {
|
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !selectedBackgroundTask) {
|
||||||
setIsChatSidebarOpen(false)
|
setIsChatSidebarOpen(false)
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
}
|
}
|
||||||
|
|
@ -4042,7 +4108,7 @@ function App() {
|
||||||
const selectedTask = selectedBackgroundTask
|
const selectedTask = selectedBackgroundTask
|
||||||
? backgroundTasks.find(t => t.name === selectedBackgroundTask)
|
? backgroundTasks.find(t => t.name === selectedBackgroundTask)
|
||||||
: null
|
: null
|
||||||
const isRightPaneContext = Boolean(selectedPath || isGraphOpen || isBrowserOpen)
|
const isRightPaneContext = Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isBrowserOpen)
|
||||||
const isRightPaneOnlyMode = isRightPaneContext && isChatSidebarOpen && isRightPaneMaximized
|
const isRightPaneOnlyMode = isRightPaneContext && isChatSidebarOpen && isRightPaneMaximized
|
||||||
const shouldCollapseLeftPane = isRightPaneOnlyMode
|
const shouldCollapseLeftPane = isRightPaneOnlyMode
|
||||||
const openMarkdownTabs = React.useMemo(() => {
|
const openMarkdownTabs = React.useMemo(() => {
|
||||||
|
|
@ -4084,7 +4150,7 @@ function App() {
|
||||||
onNewChat: handleNewChatTab,
|
onNewChat: handleNewChatTab,
|
||||||
onSelectRun: (runIdToLoad) => {
|
onSelectRun: (runIdToLoad) => {
|
||||||
cancelRecordingIfActive()
|
cancelRecordingIfActive()
|
||||||
if (selectedPath || isGraphOpen || isBrowserOpen) {
|
if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isBrowserOpen) {
|
||||||
setIsChatSidebarOpen(true)
|
setIsChatSidebarOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4095,7 +4161,7 @@ function App() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// In two-pane mode (file/graph/browser), keep the middle pane and just swap chat context in the right sidebar.
|
// In two-pane mode (file/graph/browser), keep the middle pane and just swap chat context in the right sidebar.
|
||||||
if (selectedPath || isGraphOpen || isBrowserOpen) {
|
if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isBrowserOpen) {
|
||||||
setChatTabs(prev => prev.map(t => t.id === activeChatTabId ? { ...t, runId: runIdToLoad } : t))
|
setChatTabs(prev => prev.map(t => t.id === activeChatTabId ? { ...t, runId: runIdToLoad } : t))
|
||||||
loadRun(runIdToLoad)
|
loadRun(runIdToLoad)
|
||||||
return
|
return
|
||||||
|
|
@ -4119,14 +4185,14 @@ function App() {
|
||||||
} else {
|
} else {
|
||||||
// Only one tab, reset it to new chat
|
// Only one tab, reset it to new chat
|
||||||
setChatTabs([{ id: tabForRun.id, runId: null }])
|
setChatTabs([{ id: tabForRun.id, runId: null }])
|
||||||
if (selectedPath || isGraphOpen || isBrowserOpen) {
|
if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isBrowserOpen) {
|
||||||
handleNewChat()
|
handleNewChat()
|
||||||
} else {
|
} else {
|
||||||
void navigateToView({ type: 'chat', runId: null })
|
void navigateToView({ type: 'chat', runId: null })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (runId === runIdToDelete) {
|
} else if (runId === runIdToDelete) {
|
||||||
if (selectedPath || isGraphOpen || isBrowserOpen) {
|
if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isBrowserOpen) {
|
||||||
setChatTabs(prev => prev.map(t => t.id === activeChatTabId ? { ...t, runId: null } : t))
|
setChatTabs(prev => prev.map(t => t.id === activeChatTabId ? { ...t, runId: null } : t))
|
||||||
handleNewChat()
|
handleNewChat()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -4152,6 +4218,7 @@ function App() {
|
||||||
onToggleMeeting={() => { void handleToggleMeeting() }}
|
onToggleMeeting={() => { void handleToggleMeeting() }}
|
||||||
isBrowserOpen={isBrowserOpen}
|
isBrowserOpen={isBrowserOpen}
|
||||||
onToggleBrowser={handleToggleBrowser}
|
onToggleBrowser={handleToggleBrowser}
|
||||||
|
isSuggestedTopicsOpen={isSuggestedTopicsOpen}
|
||||||
onOpenSuggestedTopics={() => void navigateToView({ type: 'suggested-topics' })}
|
onOpenSuggestedTopics={() => void navigateToView({ type: 'suggested-topics' })}
|
||||||
/>
|
/>
|
||||||
<SidebarInset
|
<SidebarInset
|
||||||
|
|
@ -4233,7 +4300,7 @@ function App() {
|
||||||
<TooltipContent side="bottom">Version history</TooltipContent>
|
<TooltipContent side="bottom">Version history</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{!selectedPath && !isGraphOpen && !selectedTask && !isBrowserOpen && (
|
{!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !selectedTask && !isBrowserOpen && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
|
|
@ -4248,7 +4315,7 @@ function App() {
|
||||||
<TooltipContent side="bottom">New chat tab</TooltipContent>
|
<TooltipContent side="bottom">New chat tab</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{!selectedPath && !isGraphOpen && !isBrowserOpen && expandedFrom && (
|
{!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isBrowserOpen && expandedFrom && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
|
|
@ -4263,7 +4330,7 @@ function App() {
|
||||||
<TooltipContent side="bottom">Restore two-pane view</TooltipContent>
|
<TooltipContent side="bottom">Restore two-pane view</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{(selectedPath || isGraphOpen) && (
|
{(selectedPath || isGraphOpen || isSuggestedTopicsOpen) && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
|
|
@ -4287,8 +4354,8 @@ function App() {
|
||||||
) : isSuggestedTopicsOpen ? (
|
) : isSuggestedTopicsOpen ? (
|
||||||
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
||||||
<SuggestedTopicsView
|
<SuggestedTopicsView
|
||||||
onExploreTopic={(title, description) => {
|
onExploreTopic={(topic) => {
|
||||||
const prompt = `I'd like to explore the topic: ${title}. ${description}`
|
const prompt = buildSuggestedTopicExplorePrompt(topic)
|
||||||
submitFromPalette(prompt, null)
|
submitFromPalette(prompt, null)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,7 @@ type SidebarContentPanelProps = {
|
||||||
onToggleMeeting?: () => void
|
onToggleMeeting?: () => void
|
||||||
isBrowserOpen?: boolean
|
isBrowserOpen?: boolean
|
||||||
onToggleBrowser?: () => void
|
onToggleBrowser?: () => void
|
||||||
|
isSuggestedTopicsOpen?: boolean
|
||||||
onOpenSuggestedTopics?: () => void
|
onOpenSuggestedTopics?: () => void
|
||||||
} & React.ComponentProps<typeof Sidebar>
|
} & React.ComponentProps<typeof Sidebar>
|
||||||
|
|
||||||
|
|
@ -418,6 +419,7 @@ export function SidebarContentPanel({
|
||||||
onToggleMeeting,
|
onToggleMeeting,
|
||||||
isBrowserOpen = false,
|
isBrowserOpen = false,
|
||||||
onToggleBrowser,
|
onToggleBrowser,
|
||||||
|
isSuggestedTopicsOpen = false,
|
||||||
onOpenSuggestedTopics,
|
onOpenSuggestedTopics,
|
||||||
...props
|
...props
|
||||||
}: SidebarContentPanelProps) {
|
}: SidebarContentPanelProps) {
|
||||||
|
|
@ -579,6 +581,21 @@ export function SidebarContentPanel({
|
||||||
<span>Run browser task</span>
|
<span>Run browser task</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{onOpenSuggestedTopics && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onOpenSuggestedTopics}
|
||||||
|
className={cn(
|
||||||
|
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors",
|
||||||
|
isSuggestedTopicsOpen
|
||||||
|
? "bg-sidebar-accent text-sidebar-accent-foreground"
|
||||||
|
: "text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Lightbulb className="size-4" />
|
||||||
|
<span>Suggested Topics</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
|
|
@ -707,18 +724,6 @@ export function SidebarContentPanel({
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{onOpenSuggestedTopics && (
|
|
||||||
<button
|
|
||||||
onClick={onOpenSuggestedTopics}
|
|
||||||
className="flex w-full items-center gap-2 rounded-md px-2 py-1 text-xs text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-colors"
|
|
||||||
>
|
|
||||||
<Lightbulb className="size-4" />
|
|
||||||
<span>Suggested Topics</span>
|
|
||||||
<span className="ml-auto rounded-full bg-blue-500/15 px-1.5 py-0.5 text-[10px] font-medium leading-none text-blue-600 dark:text-blue-400">
|
|
||||||
NEW
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<SettingsDialog>
|
<SettingsDialog>
|
||||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1 text-xs text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-colors">
|
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1 text-xs text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-colors">
|
||||||
<Settings className="size-4" />
|
<Settings className="size-4" />
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,12 @@ import { useCallback, useEffect, useState } from 'react'
|
||||||
import { ArrowRight, Lightbulb, Loader2 } from 'lucide-react'
|
import { ArrowRight, Lightbulb, Loader2 } from 'lucide-react'
|
||||||
import { SuggestedTopicBlockSchema, type SuggestedTopicBlock } from '@x/shared/dist/blocks.js'
|
import { SuggestedTopicBlockSchema, type SuggestedTopicBlock } from '@x/shared/dist/blocks.js'
|
||||||
|
|
||||||
|
const SUGGESTED_TOPICS_PATH = 'suggested-topics.md'
|
||||||
|
const LEGACY_SUGGESTED_TOPICS_PATHS = [
|
||||||
|
'config/suggested-topics.md',
|
||||||
|
'knowledge/Notes/Suggested Topics.md',
|
||||||
|
]
|
||||||
|
|
||||||
/** Parse suggestedtopic code-fence blocks from the markdown file content. */
|
/** Parse suggestedtopic code-fence blocks from the markdown file content. */
|
||||||
function parseTopics(content: string): SuggestedTopicBlock[] {
|
function parseTopics(content: string): SuggestedTopicBlock[] {
|
||||||
const topics: SuggestedTopicBlock[] = []
|
const topics: SuggestedTopicBlock[] = []
|
||||||
|
|
@ -16,13 +22,42 @@ function parseTopics(content: string): SuggestedTopicBlock[] {
|
||||||
// Skip malformed blocks
|
// Skip malformed blocks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (topics.length > 0) return topics
|
||||||
|
|
||||||
|
const lines = content
|
||||||
|
.split('\n')
|
||||||
|
.map(line => line.trim())
|
||||||
|
.filter(line => line && !line.startsWith('#'))
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(line)
|
||||||
|
const topic = SuggestedTopicBlockSchema.parse(parsed)
|
||||||
|
topics.push(topic)
|
||||||
|
} catch {
|
||||||
|
// Skip malformed lines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return topics
|
return topics
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function serializeTopics(topics: SuggestedTopicBlock[]): string {
|
||||||
|
const blocks = topics.map((topic) => [
|
||||||
|
'```suggestedtopic',
|
||||||
|
JSON.stringify(topic),
|
||||||
|
'```',
|
||||||
|
].join('\n'))
|
||||||
|
|
||||||
|
return ['# Suggested Topics', ...blocks].join('\n\n') + '\n'
|
||||||
|
}
|
||||||
|
|
||||||
const CATEGORY_COLORS: Record<string, string> = {
|
const CATEGORY_COLORS: Record<string, string> = {
|
||||||
Meetings: 'bg-violet-500/10 text-violet-600 dark:text-violet-400',
|
Meetings: 'bg-violet-500/10 text-violet-600 dark:text-violet-400',
|
||||||
Projects: 'bg-blue-500/10 text-blue-600 dark:text-blue-400',
|
Projects: 'bg-blue-500/10 text-blue-600 dark:text-blue-400',
|
||||||
People: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400',
|
People: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400',
|
||||||
|
Organizations: 'bg-cyan-500/10 text-cyan-600 dark:text-cyan-400',
|
||||||
Topics: 'bg-amber-500/10 text-amber-600 dark:text-amber-400',
|
Topics: 'bg-amber-500/10 text-amber-600 dark:text-amber-400',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,10 +68,11 @@ function getCategoryColor(category?: string): string {
|
||||||
|
|
||||||
interface TopicCardProps {
|
interface TopicCardProps {
|
||||||
topic: SuggestedTopicBlock
|
topic: SuggestedTopicBlock
|
||||||
onExplore: (topic: SuggestedTopicBlock) => void
|
onTrack: () => void
|
||||||
|
isRemoving: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function TopicCard({ topic, onExplore }: TopicCardProps) {
|
function TopicCard({ topic, onTrack, isRemoving }: TopicCardProps) {
|
||||||
return (
|
return (
|
||||||
<div className="group flex flex-col gap-3 rounded-xl border border-border/60 bg-card p-5 transition-all hover:border-border hover:shadow-sm">
|
<div className="group flex flex-col gap-3 rounded-xl border border-border/60 bg-card p-5 transition-all hover:border-border hover:shadow-sm">
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
|
@ -55,32 +91,72 @@ function TopicCard({ topic, onExplore }: TopicCardProps) {
|
||||||
{topic.description}
|
{topic.description}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => onExplore(topic)}
|
type="button"
|
||||||
className="mt-auto flex w-fit items-center gap-1.5 rounded-lg bg-primary/10 px-3 py-1.5 text-xs font-medium text-primary transition-colors hover:bg-primary/20"
|
onClick={onTrack}
|
||||||
|
disabled={isRemoving}
|
||||||
|
className="mt-auto flex w-fit items-center gap-1.5 rounded-lg bg-primary/10 px-3 py-1.5 text-xs font-medium text-primary transition-colors hover:bg-primary/20 disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
>
|
>
|
||||||
Explore
|
{isRemoving ? (
|
||||||
<ArrowRight className="size-3.5 transition-transform group-hover:translate-x-0.5" />
|
<>
|
||||||
|
<Loader2 className="size-3.5 animate-spin" />
|
||||||
|
Tracking…
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
Track
|
||||||
|
<ArrowRight className="size-3.5 transition-transform group-hover:translate-x-0.5" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SuggestedTopicsViewProps {
|
interface SuggestedTopicsViewProps {
|
||||||
onExploreTopic: (title: string, description: string) => void
|
onExploreTopic: (topic: SuggestedTopicBlock) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SuggestedTopicsView({ onExploreTopic }: SuggestedTopicsViewProps) {
|
export function SuggestedTopicsView({ onExploreTopic }: SuggestedTopicsViewProps) {
|
||||||
const [topics, setTopics] = useState<SuggestedTopicBlock[]>([])
|
const [topics, setTopics] = useState<SuggestedTopicBlock[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [removingIndex, setRemovingIndex] = useState<number | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
async function load() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
const result = await window.ipc.invoke('workspace:readFile', {
|
let result
|
||||||
path: 'config/suggested-topics.md',
|
try {
|
||||||
})
|
result = await window.ipc.invoke('workspace:readFile', {
|
||||||
|
path: SUGGESTED_TOPICS_PATH,
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
let legacyResult: { data?: string } | null = null
|
||||||
|
let legacyPath: string | null = null
|
||||||
|
for (const path of LEGACY_SUGGESTED_TOPICS_PATHS) {
|
||||||
|
try {
|
||||||
|
legacyResult = await window.ipc.invoke('workspace:readFile', { path })
|
||||||
|
legacyPath = path
|
||||||
|
break
|
||||||
|
} catch {
|
||||||
|
// Try next legacy location.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!legacyResult || !legacyPath) {
|
||||||
|
throw new Error('Suggested topics file not found')
|
||||||
|
}
|
||||||
|
await window.ipc.invoke('workspace:writeFile', {
|
||||||
|
path: SUGGESTED_TOPICS_PATH,
|
||||||
|
data: legacyResult.data,
|
||||||
|
opts: { encoding: 'utf8' },
|
||||||
|
})
|
||||||
|
await window.ipc.invoke('workspace:remove', {
|
||||||
|
path: legacyPath,
|
||||||
|
opts: { trash: true },
|
||||||
|
})
|
||||||
|
result = legacyResult
|
||||||
|
}
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
if (result.data) {
|
if (result.data) {
|
||||||
setTopics(parseTopics(result.data))
|
setTopics(parseTopics(result.data))
|
||||||
|
|
@ -95,11 +171,30 @@ export function SuggestedTopicsView({ onExploreTopic }: SuggestedTopicsViewProps
|
||||||
return () => { cancelled = true }
|
return () => { cancelled = true }
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleExplore = useCallback(
|
const handleTrack = useCallback(
|
||||||
(topic: SuggestedTopicBlock) => {
|
async (topic: SuggestedTopicBlock, topicIndex: number) => {
|
||||||
onExploreTopic(topic.title, topic.description)
|
if (removingIndex !== null) return
|
||||||
|
const nextTopics = topics.filter((_, idx) => idx !== topicIndex)
|
||||||
|
setRemovingIndex(topicIndex)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
await window.ipc.invoke('workspace:writeFile', {
|
||||||
|
path: SUGGESTED_TOPICS_PATH,
|
||||||
|
data: serializeTopics(nextTopics),
|
||||||
|
opts: { encoding: 'utf8' },
|
||||||
|
})
|
||||||
|
setTopics(nextTopics)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to remove suggested topic:', err)
|
||||||
|
setError('Failed to update suggested topics. Please try again.')
|
||||||
|
return
|
||||||
|
} finally {
|
||||||
|
setRemovingIndex(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
onExploreTopic(topic)
|
||||||
},
|
},
|
||||||
[onExploreTopic],
|
[onExploreTopic, removingIndex, topics],
|
||||||
)
|
)
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
|
@ -131,13 +226,18 @@ export function SuggestedTopicsView({ onExploreTopic }: SuggestedTopicsViewProps
|
||||||
<h2 className="text-base font-semibold text-foreground">Suggested Topics</h2>
|
<h2 className="text-base font-semibold text-foreground">Suggested Topics</h2>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
Topics surfaced from your knowledge graph. Explore them to create new notes.
|
Suggested notes surfaced from your knowledge graph. Track one to start a tracking note.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto p-6">
|
<div className="flex-1 overflow-y-auto p-6">
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{topics.map((topic, i) => (
|
{topics.map((topic, i) => (
|
||||||
<TopicCard key={`${topic.title}-${i}`} topic={topic} onExplore={handleExplore} />
|
<TopicCard
|
||||||
|
key={`${topic.title}-${i}`}
|
||||||
|
topic={topic}
|
||||||
|
onTrack={() => { void handleTrack(topic, i) }}
|
||||||
|
isRemoving={removingIndex === i}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,21 @@ Workflow:
|
||||||
|
|
||||||
Ask one question: "Which note should this track live in?" Don't create a new note unless the user asks.
|
Ask one question: "Which note should this track live in?" Don't create a new note unless the user asks.
|
||||||
|
|
||||||
|
### Suggested Topics exploration flow
|
||||||
|
|
||||||
|
Sometimes the user arrives from the Suggested Topics panel and gives you a prompt like:
|
||||||
|
- "I am exploring a suggested topic card from the Suggested Topics panel."
|
||||||
|
- a title, category, description, and target folder such as ` + "`" + `knowledge/Topics/` + "`" + ` or ` + "`" + `knowledge/People/` + "`" + `
|
||||||
|
|
||||||
|
In that flow:
|
||||||
|
1. On the first turn, **do not create or modify anything yet**. Briefly explain the tracking note you can set up and ask for confirmation.
|
||||||
|
2. If the user clearly confirms ("yes", "set it up", "do it"), treat that as explicit permission to proceed.
|
||||||
|
3. Before creating a new note, search the target folder for an existing matching note and update it if one already exists.
|
||||||
|
4. If no matching note exists and the prompt gave you a target folder, create the new note there without bouncing back to ask "which note should this live in?".
|
||||||
|
5. Use the card title as the default note title / filename unless a small normalization is clearly needed.
|
||||||
|
6. Keep the surrounding note scaffolding minimal but useful. The track block should be the core of the note.
|
||||||
|
7. If the target folder is one of the structured knowledge folders (` + "`" + `knowledge/People/` + "`" + `, ` + "`" + `knowledge/Organizations/` + "`" + `, ` + "`" + `knowledge/Projects/` + "`" + `, ` + "`" + `knowledge/Topics/` + "`" + `), mirror the local note style by quickly checking a nearby note or config before writing if needed.
|
||||||
|
|
||||||
## The Exact Text to Insert
|
## The Exact Text to Insert
|
||||||
|
|
||||||
Write it verbatim like this (including the blank line between fence and target):
|
Write it verbatim like this (including the blank line between fence and target):
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,12 @@ import { getTagDefinitions } from './tag_system.js';
|
||||||
|
|
||||||
const NOTES_OUTPUT_DIR = path.join(WorkDir, 'knowledge');
|
const NOTES_OUTPUT_DIR = path.join(WorkDir, 'knowledge');
|
||||||
const NOTE_CREATION_AGENT = 'note_creation';
|
const NOTE_CREATION_AGENT = 'note_creation';
|
||||||
|
const SUGGESTED_TOPICS_REL_PATH = 'suggested-topics.md';
|
||||||
|
const SUGGESTED_TOPICS_PATH = path.join(WorkDir, 'suggested-topics.md');
|
||||||
|
const LEGACY_SUGGESTED_TOPICS_REL_PATH = 'config/suggested-topics.md';
|
||||||
|
const LEGACY_SUGGESTED_TOPICS_PATH = path.join(WorkDir, 'config', 'suggested-topics.md');
|
||||||
|
const LEGACY_SUGGESTED_TOPICS_KNOWLEDGE_REL_PATH = 'knowledge/Notes/Suggested Topics.md';
|
||||||
|
const LEGACY_SUGGESTED_TOPICS_KNOWLEDGE_PATH = path.join(WorkDir, 'knowledge', 'Notes', 'Suggested Topics.md');
|
||||||
|
|
||||||
// Configuration for the graph builder service
|
// Configuration for the graph builder service
|
||||||
const SYNC_INTERVAL_MS = 15 * 1000; // 15 seconds
|
const SYNC_INTERVAL_MS = 15 * 1000; // 15 seconds
|
||||||
|
|
@ -88,6 +94,49 @@ function extractPathFromToolInput(input: string): string | null {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureSuggestedTopicsFileLocation(): string {
|
||||||
|
if (fs.existsSync(SUGGESTED_TOPICS_PATH)) {
|
||||||
|
return SUGGESTED_TOPICS_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
const legacyCandidates: Array<{ absPath: string; relPath: string }> = [
|
||||||
|
{ absPath: LEGACY_SUGGESTED_TOPICS_PATH, relPath: LEGACY_SUGGESTED_TOPICS_REL_PATH },
|
||||||
|
{ absPath: LEGACY_SUGGESTED_TOPICS_KNOWLEDGE_PATH, relPath: LEGACY_SUGGESTED_TOPICS_KNOWLEDGE_REL_PATH },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const legacy of legacyCandidates) {
|
||||||
|
if (!fs.existsSync(legacy.absPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.renameSync(legacy.absPath, SUGGESTED_TOPICS_PATH);
|
||||||
|
console.log(`[buildGraph] Moved suggested topics file from ${legacy.relPath} to ${SUGGESTED_TOPICS_REL_PATH}`);
|
||||||
|
return SUGGESTED_TOPICS_PATH;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[buildGraph] Failed to move suggested topics file from ${legacy.relPath} to ${SUGGESTED_TOPICS_REL_PATH}:`, error);
|
||||||
|
return legacy.absPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SUGGESTED_TOPICS_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readSuggestedTopicsFile(): string {
|
||||||
|
try {
|
||||||
|
const suggestedTopicsPath = ensureSuggestedTopicsFileLocation();
|
||||||
|
if (!fs.existsSync(suggestedTopicsPath)) {
|
||||||
|
return '_No existing suggested topics file._';
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fs.readFileSync(suggestedTopicsPath, 'utf-8').trim();
|
||||||
|
return content.length > 0 ? content : '_Existing suggested topics file is empty._';
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[buildGraph] Error reading suggested topics file:`, error);
|
||||||
|
return '_Failed to read existing suggested topics file._';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get unprocessed voice memo files from knowledge/Voice Memos/
|
* Get unprocessed voice memo files from knowledge/Voice Memos/
|
||||||
* Voice memos are created directly in this directory by the UI.
|
* Voice memos are created directly in this directory by the UI.
|
||||||
|
|
@ -203,6 +252,7 @@ async function createNotesFromBatch(
|
||||||
const run = await createRun({
|
const run = await createRun({
|
||||||
agentId: NOTE_CREATION_AGENT,
|
agentId: NOTE_CREATION_AGENT,
|
||||||
});
|
});
|
||||||
|
const suggestedTopicsContent = readSuggestedTopicsFile();
|
||||||
|
|
||||||
// Build message with index and all files in the batch
|
// Build message with index and all files in the batch
|
||||||
let message = `Process the following ${files.length} source files and create/update obsidian notes.\n\n`;
|
let message = `Process the following ${files.length} source files and create/update obsidian notes.\n\n`;
|
||||||
|
|
@ -210,8 +260,9 @@ async function createNotesFromBatch(
|
||||||
message += `- Use the KNOWLEDGE BASE INDEX below to resolve entities - DO NOT grep/search for existing notes\n`;
|
message += `- Use the KNOWLEDGE BASE INDEX below to resolve entities - DO NOT grep/search for existing notes\n`;
|
||||||
message += `- Extract entities (people, organizations, projects, topics) from ALL files below\n`;
|
message += `- Extract entities (people, organizations, projects, topics) from ALL files below\n`;
|
||||||
message += `- Create or update notes in "knowledge" directory (workspace-relative paths like "knowledge/People/Name.md")\n`;
|
message += `- Create or update notes in "knowledge" directory (workspace-relative paths like "knowledge/People/Name.md")\n`;
|
||||||
|
message += `- You may also create or update "${SUGGESTED_TOPICS_REL_PATH}" to maintain curated suggested-topic cards\n`;
|
||||||
message += `- If the same entity appears in multiple files, merge the information into a single note\n`;
|
message += `- If the same entity appears in multiple files, merge the information into a single note\n`;
|
||||||
message += `- Use workspace tools to read existing notes (when you need full content) and write updates\n`;
|
message += `- Use workspace tools to read existing notes or "${SUGGESTED_TOPICS_REL_PATH}" (when you need full content) and write updates\n`;
|
||||||
message += `- Follow the note templates and guidelines in your instructions\n\n`;
|
message += `- Follow the note templates and guidelines in your instructions\n\n`;
|
||||||
|
|
||||||
// Add the knowledge base index
|
// Add the knowledge base index
|
||||||
|
|
@ -219,6 +270,11 @@ async function createNotesFromBatch(
|
||||||
message += knowledgeIndex;
|
message += knowledgeIndex;
|
||||||
message += `\n---\n\n`;
|
message += `\n---\n\n`;
|
||||||
|
|
||||||
|
message += `# Current Suggested Topics File\n\n`;
|
||||||
|
message += `Path: ${SUGGESTED_TOPICS_REL_PATH}\n\n`;
|
||||||
|
message += suggestedTopicsContent;
|
||||||
|
message += `\n\n---\n\n`;
|
||||||
|
|
||||||
// Add each file's content
|
// Add each file's content
|
||||||
message += `# Source Files to Process\n\n`;
|
message += `# Source Files to Process\n\n`;
|
||||||
files.forEach((file, idx) => {
|
files.forEach((file, idx) => {
|
||||||
|
|
|
||||||
|
|
@ -485,9 +485,9 @@ RESOLVED (use canonical name with absolute path):
|
||||||
- "Acme", "Acme Corp", "@acme.com" → [[Organizations/Acme Corp]]
|
- "Acme", "Acme Corp", "@acme.com" → [[Organizations/Acme Corp]]
|
||||||
- "the pilot", "the integration" → [[Projects/Acme Integration]]
|
- "the pilot", "the integration" → [[Projects/Acme Integration]]
|
||||||
|
|
||||||
NEW ENTITIES (create notes if source passes filters):
|
NEW ENTITIES (create notes or suggestion cards if source passes filters):
|
||||||
- "Jennifer" (CTO, Acme Corp) → Create [[People/Jennifer]] or [[People/Jennifer (Acme Corp)]]
|
- "Jennifer" (CTO, Acme Corp) → Create [[People/Jennifer]] or [[People/Jennifer (Acme Corp)]]
|
||||||
- "SOC 2" → Create [[Topics/Security Compliance]]
|
- "SOC 2" → Add or update a suggestion card in \`suggested-topics.md\` with category \`Topics\`
|
||||||
|
|
||||||
AMBIGUOUS (flag or skip):
|
AMBIGUOUS (flag or skip):
|
||||||
- "Mike" (no context) → Mention in activity only, don't create note
|
- "Mike" (no context) → Mention in activity only, don't create note
|
||||||
|
|
@ -508,8 +508,8 @@ For entities not resolved to existing notes, determine if they warrant new notes
|
||||||
|
|
||||||
**CREATE a note for people who are:**
|
**CREATE a note for people who are:**
|
||||||
- External (not @user.domain)
|
- External (not @user.domain)
|
||||||
- Attendees in meetings
|
- People you directly interacted with in meetings
|
||||||
- Email correspondents (emails that reach this step already passed label-based filtering)
|
- Email correspondents directly participating in the thread (emails that reach this step already passed label-based filtering)
|
||||||
- Decision makers or contacts at customers, prospects, or partners
|
- Decision makers or contacts at customers, prospects, or partners
|
||||||
- Investors or potential investors
|
- Investors or potential investors
|
||||||
- Candidates you are interviewing
|
- Candidates you are interviewing
|
||||||
|
|
@ -521,6 +521,7 @@ For entities not resolved to existing notes, determine if they warrant new notes
|
||||||
- Large group meeting attendees you didn't interact with
|
- Large group meeting attendees you didn't interact with
|
||||||
- Internal colleagues (@user.domain)
|
- Internal colleagues (@user.domain)
|
||||||
- Assistants handling only logistics
|
- Assistants handling only logistics
|
||||||
|
- People mentioned only as third parties ("we work with X", "I can introduce you to Y") when there has been no direct interaction yet
|
||||||
|
|
||||||
### Role Inference
|
### Role Inference
|
||||||
|
|
||||||
|
|
@ -579,31 +580,155 @@ For people who don't warrant their own note, add to Organization note's Contacts
|
||||||
- Sarah Lee — Support, handled wire transfer issue
|
- Sarah Lee — Support, handled wire transfer issue
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
|
### Direct Interaction Test (People and Organizations)
|
||||||
|
|
||||||
|
For **new canonical People and Organizations notes**, require **direct interaction**, not just mention.
|
||||||
|
|
||||||
|
**Direct interaction = YES**
|
||||||
|
- The person sent the email, replied in the thread, or was directly addressed as part of the active exchange
|
||||||
|
- The person participated in the meeting, and there is evidence the user actually interacted with them or the meeting centered on them
|
||||||
|
- The organization is directly represented in the exchange by participants/senders and is part of an active first-degree relationship with the user or team
|
||||||
|
- The user is directly evaluating, selling to, buying from, partnering with, interviewing, or coordinating with that person or organization
|
||||||
|
|
||||||
|
**Direct interaction = NO**
|
||||||
|
- Someone else mentions them in passing
|
||||||
|
- A sender says they work with someone at another company
|
||||||
|
- A sender offers to introduce the user to someone
|
||||||
|
- A company is referenced as a customer, partner, employer, competitor, or example, but nobody from that company is directly involved in the interaction
|
||||||
|
- The source only establishes a second-degree relationship, not a direct one
|
||||||
|
|
||||||
|
**Canonical note rule:**
|
||||||
|
- For **new People/Organizations**, create the canonical note only if both are true:
|
||||||
|
1. There is **direct interaction**
|
||||||
|
2. The entity clears the **weekly importance test**
|
||||||
|
|
||||||
|
If an entity seems strategically relevant but fails the direct interaction test, do **not** auto-create a canonical note. At most, create a suggestion card in \`suggested-topics.md\`.
|
||||||
|
|
||||||
|
### Weekly Importance Test (People and Organizations only)
|
||||||
|
|
||||||
|
For **People** and **Organizations**, the final gate for **creating a new canonical note** is an importance test:
|
||||||
|
|
||||||
|
**Ask:** _"If I were the user, would I realistically need to look at this note on a weekly basis over the near term?"_
|
||||||
|
|
||||||
|
This test is mainly for **People** and **Organizations**. **Do NOT use it as the decision rule for Topic or Project suggestions.**
|
||||||
|
|
||||||
|
**Strong YES signals:**
|
||||||
|
- Active customer, prospect, investor, partner, candidate, advisor, or strategic vendor relationship
|
||||||
|
- Repeated interaction or a likely ongoing cadence
|
||||||
|
- Decision-maker, owner, blocker, evaluator, or approver in an active process
|
||||||
|
- Material relevance to launch, sales, fundraising, hiring, compliance, product delivery, or another current priority
|
||||||
|
- The user would benefit from a durable reference note instead of repeatedly reopening raw emails or meeting transcripts
|
||||||
|
|
||||||
|
**Strong NO signals:**
|
||||||
|
- One-off logistics, scheduling, or transactional contact
|
||||||
|
- Assistant, support rep, recruiter, or vendor rep with no ongoing strategic role
|
||||||
|
- Incidental attendee mentioned once with no leverage on current work
|
||||||
|
- Passing mention with no evidence of an ongoing relationship
|
||||||
|
|
||||||
|
**Borderline signals:**
|
||||||
|
- Seems potentially important, but there isn't enough evidence yet that the user will need a weekly reference note
|
||||||
|
- Might become important soon, but the role, relationship, or repeated relevance is still unclear
|
||||||
|
- Important enough to track, but only through second-degree mention or an offered introduction rather than direct interaction
|
||||||
|
|
||||||
|
**Outcome rules for new People/Organizations:**
|
||||||
|
- **Clear YES + direct interaction** → Create/update the canonical \`People/\` or \`Organizations/\` note
|
||||||
|
- **Borderline or no direct interaction, but still strategically relevant** → Do **not** create the canonical note yet; instead create or update a card in \`suggested-topics.md\`
|
||||||
|
- **Clear NO** → Skip note creation and do not add a suggestion unless the source strongly suggests near-term strategic relevance
|
||||||
|
|
||||||
|
**When a canonical note already exists:**
|
||||||
|
- Update the existing note even if the current source is weaker; the importance test is mainly for deciding whether to create a **new** People/Organization note
|
||||||
|
- If a previously tentative person/org is now clearly important enough for a canonical note, create/update the note and remove any tentative suggestion card for that exact entity from \`suggested-topics.md\`
|
||||||
|
|
||||||
## Organizations
|
## Organizations
|
||||||
|
|
||||||
**CREATE a note if:**
|
**CREATE a note if:**
|
||||||
- Someone from that org attended a meeting
|
- There is direct interaction with that org in the source
|
||||||
- They're a customer, prospect, investor, or partner
|
- They're a customer, prospect, investor, or partner in a direct first-degree interaction
|
||||||
- Someone from that org sent relevant personalized correspondence
|
- Someone from that org sent relevant personalized correspondence or joined a meeting you actually had with them
|
||||||
|
- They pass the weekly importance test above
|
||||||
|
|
||||||
**DO NOT create for:**
|
**DO NOT create for:**
|
||||||
- Tool/service providers mentioned in passing
|
- Tool/service providers mentioned in passing
|
||||||
- One-time transactional vendors
|
- One-time transactional vendors
|
||||||
- Consumer service companies
|
- Consumer service companies
|
||||||
|
- Organizations only referenced through third-party mention or offered introductions
|
||||||
|
|
||||||
## Projects
|
## Projects
|
||||||
|
|
||||||
**CREATE a note if:**
|
**If a project note already exists:** update it.
|
||||||
|
|
||||||
|
**If no project note exists:** do **not** create a new canonical note in \`knowledge/Projects/\`.
|
||||||
|
|
||||||
|
Instead, create or update a **suggestion card** in \`suggested-topics.md\` if the project is strong enough:
|
||||||
- Discussed substantively in a meeting or email thread
|
- Discussed substantively in a meeting or email thread
|
||||||
- Has a goal and timeline
|
- Has a goal and timeline
|
||||||
- Involves multiple interactions
|
- Involves multiple interactions
|
||||||
|
|
||||||
|
Otherwise skip it.
|
||||||
|
|
||||||
|
Projects do **not** use the weekly importance test above. For **new** projects, the default output is a suggestion card, not a canonical note.
|
||||||
|
|
||||||
## Topics
|
## Topics
|
||||||
|
|
||||||
**CREATE a note if:**
|
**If a topic note already exists:** update it.
|
||||||
|
|
||||||
|
**If no topic note exists:** do **not** create a new canonical note in \`knowledge/Topics/\`.
|
||||||
|
|
||||||
|
Instead, create or update a **suggestion card** in \`suggested-topics.md\` if the topic is strong enough:
|
||||||
- Recurring theme discussed
|
- Recurring theme discussed
|
||||||
- Will come up again across conversations
|
- Will come up again across conversations
|
||||||
|
|
||||||
|
Otherwise skip it.
|
||||||
|
|
||||||
|
Topics do **not** use the weekly importance test above. For **new** topics, the default output is a suggestion card, not a canonical note.
|
||||||
|
|
||||||
|
## Suggested Topics Curation
|
||||||
|
|
||||||
|
Also maintain \`suggested-topics.md\` as a **curated shortlist** of things worth exploring next.
|
||||||
|
|
||||||
|
Despite the filename, \`suggested-topics.md\` can contain cards for **People, Organizations, Topics, or Projects**.
|
||||||
|
|
||||||
|
There are **two reasons** to add or update a suggestion card:
|
||||||
|
|
||||||
|
1. **High-quality Topic/Project cards**
|
||||||
|
- Use these for topics or projects that are timely, high-leverage, strategically important, or clearly worth exploring now
|
||||||
|
- These are not a dump of every topic/project note. Be selective
|
||||||
|
- For **new** topics and projects, cards are the default output from this pipeline
|
||||||
|
|
||||||
|
2. **Tentative People/Organization cards**
|
||||||
|
- Use these when a person or organization seems important enough to track, but you are **not 100% sure** they clear the weekly-importance test for a canonical note yet
|
||||||
|
- The card should capture why they might matter and what still needs verification
|
||||||
|
|
||||||
|
**Do NOT add cards for:**
|
||||||
|
- Low-signal administrative or transactional entities
|
||||||
|
- Stale or completed items with no near-term relevance
|
||||||
|
- People/organizations that already have a clearly established canonical note, unless the card is about a distinct project/topic exploration rather than the entity itself
|
||||||
|
|
||||||
|
**Card guidance:**
|
||||||
|
- For **Topics/Projects**, use category \`Topics\` or \`Projects\`
|
||||||
|
- For tentative **People/Organizations**, use category \`People\` or \`Organizations\`
|
||||||
|
- Title should be concise and canonical when possible
|
||||||
|
- Description should explain why it matters **now**
|
||||||
|
- For tentative People/Organizations, description should also mention what is still uncertain or what the user should verify
|
||||||
|
|
||||||
|
**Curation rules:**
|
||||||
|
- Maintain a **high-quality set**, not an ever-growing backlog
|
||||||
|
- Deduplicate by normalized title
|
||||||
|
- Prefer current, actionable, recurring, or strategically important items
|
||||||
|
- Keep only the strongest **8-12 cards total**
|
||||||
|
- Preserve good existing cards unless the new source clearly supersedes them
|
||||||
|
- Remove stale cards that are no longer relevant
|
||||||
|
- If a tentative People/Organization card later becomes clearly important and you create a canonical note, remove the tentative card
|
||||||
|
|
||||||
|
**File format for \`suggested-topics.md\`:**
|
||||||
|
\`\`\`suggestedtopic
|
||||||
|
{"title":"Security Compliance","description":"Summarize the current compliance posture, blockers, and customer implications.","category":"Topics"}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
The file should start with \`# Suggested Topics\` followed by one or more blocks in that format.
|
||||||
|
|
||||||
|
If the file does not exist, create it. If it exists, update it in place or rewrite the full file so the final result is clean, deduped, and curated.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Step 6: Extract Content
|
# Step 6: Extract Content
|
||||||
|
|
@ -824,7 +949,7 @@ If new info contradicts existing:
|
||||||
|
|
||||||
# Step 9: Write Updates
|
# Step 9: Write Updates
|
||||||
|
|
||||||
## 9a: Create and Update Notes
|
## 9a: Create and Update Notes and Suggested Topic Cards
|
||||||
|
|
||||||
**IMPORTANT: Write sequentially, one file at a time.**
|
**IMPORTANT: Write sequentially, one file at a time.**
|
||||||
- Generate content for exactly one note.
|
- Generate content for exactly one note.
|
||||||
|
|
@ -852,6 +977,12 @@ workspace-edit({
|
||||||
})
|
})
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
|
**For \`suggested-topics.md\`:**
|
||||||
|
- Use workspace-relative path \`suggested-topics.md\`
|
||||||
|
- Read the current file if you need the latest content
|
||||||
|
- Use \`workspace-writeFile\` to create or rewrite the file when that is simpler and cleaner
|
||||||
|
- Use \`workspace-edit\` for small targeted edits only if that keeps the file deduped and readable
|
||||||
|
|
||||||
## 9b: Apply State Changes
|
## 9b: Apply State Changes
|
||||||
|
|
||||||
For each state change identified in Step 7, update the relevant fields.
|
For each state change identified in Step 7, update the relevant fields.
|
||||||
|
|
@ -867,8 +998,9 @@ If you discovered new name variants during resolution, add them to Aliases field
|
||||||
- Be concise: one line per activity entry
|
- Be concise: one line per activity entry
|
||||||
- Note state changes with \`[Field → value]\` in activity
|
- Note state changes with \`[Field → value]\` in activity
|
||||||
- Escape quotes properly in shell commands
|
- Escape quotes properly in shell commands
|
||||||
- Write only one file per response (no multi-file write batches)
|
- Write only one file per response (notes and \`suggested-topics.md\` follow the same rule)
|
||||||
- **Always set \`Last update\`** in the Info section to the YYYY-MM-DD date of the source email or meeting. When updating an existing note, update this field to the new source event's date.
|
- **Always set \`Last update\`** in the Info section to the YYYY-MM-DD date of the source email or meeting. When updating an existing note, update this field to the new source event's date.
|
||||||
|
- Keep \`suggested-topics.md\` curated, deduped, and capped to the strongest 8-12 cards
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -957,8 +1089,12 @@ Before completing, verify:
|
||||||
**Filtering:**
|
**Filtering:**
|
||||||
- [ ] Excluded self (user.name, user.email, @user.domain)
|
- [ ] Excluded self (user.name, user.email, @user.domain)
|
||||||
- [ ] Applied relevance test to each person
|
- [ ] Applied relevance test to each person
|
||||||
|
- [ ] Applied the direct interaction test to new People/Organizations
|
||||||
|
- [ ] Applied the weekly importance test to new People/Organizations
|
||||||
- [ ] Transactional contacts in Org Contacts, not People notes
|
- [ ] Transactional contacts in Org Contacts, not People notes
|
||||||
- [ ] Source correctly classified (process vs skip)
|
- [ ] Source correctly classified (process vs skip)
|
||||||
|
- [ ] Third-party mentions did not become new canonical People/Organizations notes
|
||||||
|
- [ ] Borderline People/Organizations became suggestion cards instead of canonical notes
|
||||||
|
|
||||||
**Content Quality:**
|
**Content Quality:**
|
||||||
- [ ] Summaries describe relationship, not communication method
|
- [ ] Summaries describe relationship, not communication method
|
||||||
|
|
@ -978,6 +1114,9 @@ Before completing, verify:
|
||||||
- [ ] All entity mentions use \`[[Folder/Name]]\` absolute links
|
- [ ] All entity mentions use \`[[Folder/Name]]\` absolute links
|
||||||
- [ ] Activity entries are reverse chronological
|
- [ ] Activity entries are reverse chronological
|
||||||
- [ ] No duplicate activity entries
|
- [ ] No duplicate activity entries
|
||||||
|
- [ ] \`suggested-topics.md\` stays deduped and curated
|
||||||
|
- [ ] High-quality Topics/Projects were added to suggested topics only when timely and useful
|
||||||
|
- [ ] New Topics/Projects were not auto-created as canonical notes
|
||||||
- [ ] Dates are YYYY-MM-DD
|
- [ ] Dates are YYYY-MM-DD
|
||||||
- [ ] Bidirectional links are consistent
|
- [ ] Bidirectional links are consistent
|
||||||
- [ ] New notes in correct folders
|
- [ ] New notes in correct folders
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue