-
Notifications
You must be signed in to change notification settings - Fork 169
feat: hierarchical prompt structure with drag-drop and keyboard indentation #172
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -249,4 +249,10 @@ export function registerPromptIPC(db: PromptDB, folderDb: FolderDB, rawDb: Datab | |
| db.insertVersionDirect(version); | ||
| return true; | ||
| }); | ||
|
|
||
| ipcMain.handle(IPC_CHANNELS.PROMPT_MOVE, async (_, promptId: string, newParentId: string | null, newOrder: number) => { | ||
| db.movePrompt(promptId, newParentId, newOrder); | ||
| syncWorkspace(); | ||
| return true; | ||
| }); | ||
|
Comment on lines
+253
to
+257
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3. Prompt_move ipc unvalidated The new IPC_CHANNELS.PROMPT_MOVE handler accepts promptId, newParentId, and newOrder without validating payload types/ranges before calling database logic. Malformed IPC inputs could cause runtime errors or unintended DB updates across the process boundary. Agent Prompt
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,7 @@ const PromptQuickRewriteDialog = lazy(() => import('../prompt/PromptQuickRewrite | |
| const PromptTableView = lazy(() => import('../prompt/PromptTableView').then(m => ({ default: m.PromptTableView }))); | ||
| const PromptGalleryView = lazy(() => import('../prompt/PromptGalleryView').then(m => ({ default: m.PromptGalleryView }))); | ||
| const PromptKanbanView = lazy(() => import('../prompt/PromptKanbanView').then(m => ({ default: m.PromptKanbanView }))); | ||
| const PromptListView = lazy(() => import('../prompt/PromptListView').then(m => ({ default: m.PromptListView }))); | ||
| const AiTestModal = lazy(() => import('../prompt/AiTestModal').then(m => ({ default: m.AiTestModal }))); | ||
| const PromptDetailModal = lazy(() => import('../prompt/PromptDetailModal').then(m => ({ default: m.PromptDetailModal }))); | ||
| const VariableInputModal = lazy(() => import('../prompt/VariableInputModal').then(m => ({ default: m.VariableInputModal }))); | ||
|
|
@@ -367,6 +368,7 @@ function PromptSkillMainContent() { | |
| const sortOrder = usePromptStore((state) => state.sortOrder); | ||
| const viewMode = usePromptStore((state) => state.viewMode); | ||
| const incrementUsageCount = usePromptStore((state) => state.incrementUsageCount); | ||
| const movePrompt = usePromptStore((state) => state.movePrompt); | ||
| // Resizable prompt-list pane width (#119) | ||
| const promptListPaneWidth = useUIStore((state) => state.promptListPaneWidth); | ||
| const setPromptListPaneWidth = useUIStore( | ||
|
|
@@ -2014,6 +2016,28 @@ function PromptSkillMainContent() { | |
| )} | ||
| </div> | ||
|
|
||
| {/* List view mode: hierarchical list with drag-and-drop */} | ||
| {/* 列表视图模式:分层列表支持拖拽 */} | ||
| <div | ||
|
Comment on lines
+2019
to
+2021
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1. Chinese comments in source Chinese text was added directly in non-locale source files, which violates the rule forbidding Chinese characters outside locale JSON. This can lead to untranslated/inconsistent language output and fails the localization completeness requirement. Agent Prompt
|
||
| className={getViewClass('list')} | ||
| > | ||
| <PromptListHeader count={sortedPrompts.length} /> | ||
| {viewMode === 'list' && ( | ||
| <Suspense fallback={loadingFallback}> | ||
| <PromptListView | ||
| prompts={visiblePrompts} | ||
| selectedId={selectedId} | ||
| selectedIds={selectedIds} | ||
| onSelect={(id) => selectPrompt(id)} | ||
| onToggleFavorite={toggleFavorite} | ||
| onCopy={handleCopyPrompt} | ||
| onContextMenu={handleContextMenu} | ||
| onMovePrompt={movePrompt} | ||
| /> | ||
| </Suspense> | ||
| )} | ||
| </div> | ||
|
Comment on lines
+2019
to
+2039
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
这里新增了第二个 🤖 Prompt for AI Agents |
||
|
|
||
| {/* Card view mode: two-column layout */} | ||
| {/* 卡片视图模式:左右分栏 */} | ||
| <div | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,28 +1,73 @@ | ||||||||||||||||||||||
| import { useState, useCallback, useEffect } from 'react'; | ||||||||||||||||||||||
| import { useTranslation } from 'react-i18next'; | ||||||||||||||||||||||
| import { StarIcon, CopyIcon, ImageIcon } from 'lucide-react'; | ||||||||||||||||||||||
| import { StarIcon, CopyIcon, ImageIcon, ChevronRight, ChevronDown, GripVertical } from 'lucide-react'; | ||||||||||||||||||||||
| import type { Prompt } from '@prompthub/shared/types'; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| interface PromptListViewProps { | ||||||||||||||||||||||
| prompts: Prompt[]; | ||||||||||||||||||||||
| selectedId: string | null; | ||||||||||||||||||||||
| selectedIds: string[]; | ||||||||||||||||||||||
| onSelect: (id: string) => void; | ||||||||||||||||||||||
| onToggleFavorite: (id: string) => void; | ||||||||||||||||||||||
| onCopy: (prompt: Prompt) => void; | ||||||||||||||||||||||
| onContextMenu: (e: React.MouseEvent, prompt: Prompt) => void; | ||||||||||||||||||||||
| onMovePrompt: (promptId: string, newParentId: string | null, newOrder: number) => void; | ||||||||||||||||||||||
| sortBy?: string; | ||||||||||||||||||||||
| sortOrder?: 'asc' | 'desc'; | ||||||||||||||||||||||
|
Comment on lines
+14
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
repo_root="$(pwd)"
file="apps/desktop/src/renderer/components/prompt/PromptListView.tsx"
echo "== File exists? =="
ls -l "$file"
echo "== Snippet around lines 1-220 =="
sed -n '1,220p' "$file" | nl -ba | sed -n '1,220p'
echo "== Grep onMovePrompt occurrences =="
rg -n "onMovePrompt" "$file"
echo "== Find where onMovePrompt is passed to MainContent (search MainContent props) =="
rg -n "MainContent" "$file" -SRepository: legeling/PromptHub Length of output: 289 🏁 Script executed: #!/bin/bash
set -euo pipefail
repo_root="$(pwd)"
file="apps/desktop/src/renderer/components/prompt/PromptListView.tsx"
echo "== File exists? =="
ls -l "$file"
echo "== Snippet around start to 220 =="
sed -n '1,220p' "$file" | nl -ba
echo "== Grep onMovePrompt occurrences =="
rg -n "onMovePrompt" "$file"
echo "== Grep MainContent =="
rg -n "MainContent" "$file" -SRepository: legeling/PromptHub Length of output: 290 🏁 Script executed: #!/bin/bash
set -euo pipefail
repo_root="$(pwd)"
file="apps/desktop/src/renderer/components/prompt/PromptListView.tsx"
ls -l "$file"
sed -n '1,240p' "$file" | nl -ba
rg -n "onMovePrompt" "$file"
rg -n "MainContent" "$file" -SRepository: legeling/PromptHub Length of output: 236 🏁 Script executed: #!/bin/bash
set -euo pipefail
file="apps/desktop/src/renderer/components/prompt/PromptListView.tsx"
echo "== File =="
ls -l "$file"
echo "== Lines 1-220 =="
sed -n '1,220p' "$file" | nl -ba
echo "== onMovePrompt =="
rg -n "onMovePrompt" "$file"
echo "== MainContent =="
rg -n "MainContent" "$file" -SRepository: legeling/PromptHub Length of output: 265 🏁 Script executed: #!/bin/bash
set -euo pipefail
file="apps/desktop/src/renderer/components/prompt/PromptListView.tsx"
# show relevant ranges mentioned: 1-90, 35-110, 130-210
for start in 1 35 120 140; do
end=$((start+70))
echo "== $file lines $start-$end =="
sed -n "${start},${end}p" "$file" | nl -ba
done
echo "== all onMovePrompt occurrences =="
rg -n "onMovePrompt" "$file"
echo "== all invocations calling onMovePrompt (looking for identifier usage) =="
rg -n "onMovePrompt\s*\(" "$file"Repository: legeling/PromptHub Length of output: 203 🏁 Script executed: ls -lRepository: legeling/PromptHub Length of output: 1083 🏁 Script executed: file="apps/desktop/src/renderer/components/prompt/PromptListView.tsx"
sed -n '1,220p' "$file" | nl -ba
rg -n "onMovePrompt" "$file"
rg -n "onMovePrompt\s*\(" "$file"
rg -n "keyboard|drag|drop|onDrop|onKey|Enter|Space" "$file" -SRepository: legeling/PromptHub Length of output: 3005 🏁 Script executed: file="apps/desktop/src/renderer/components/prompt/PromptListView.tsx"
echo "== Content 1-90 =="
sed -n '1,90p' "$file" | awk '{printf "%4d: %s\n", NR, $0}'
echo "== Content 90-140 =="
sed -n '90,140p' "$file" | awk '{printf "%4d: %s\n", NR+90, $0}'
echo "== Content 140-210 =="
sed -n '140,210p' "$file" | awk '{printf "%4d: %s\n", NR+140, $0}'
echo "== Content 210-260 =="
sed -n '210,260p' "$file" | awk '{printf "%4d: %s\n", NR+210, $0}'
echo "== Search keyboard handlers in file (key/KeyDown/Enter/Space) =="
rg -n "onKeyDown|onKeyUp|keydown|KeyDown|Enter|Space" "$file" -S
echo "== Search for onMovePrompt assigned/returned in repo (apps/desktop/src) =="
rg -n "onMovePrompt" apps/desktop/src -SRepository: legeling/PromptHub Length of output: 13465 🏁 Script executed: file="apps/desktop/src/renderer/components/prompt/PromptListView.tsx"
echo "== Content 1-90 =="
sed -n '1,90p' "$file" | awk '{printf "%4d: %s\n", NR, $0}'
echo "== Content 90-140 =="
sed -n '90,140p' "$file" | awk '{printf "%4d: %s\n", NR+90, $0}'
echo "== Content 140-210 =="
sed -n '140,210p' "$file" | awk '{printf "%4d: %s\n", NR+140, $0}'
echo "== Content 210-260 =="
sed -n '210,260p' "$file" | awk '{printf "%4d: %s\n", NR+210, $0}'
echo "== Search keyboard handlers in file (key/KeyDown/Enter/Space) =="
rg -n "onKeyDown|onKeyUp|keydown|KeyDown|Enter|Space" "$file" -S
echo "== Search for onMovePrompt assigned/returned in repo (apps/desktop/src) =="
rg -n "onMovePrompt" apps/desktop/src -SRepository: legeling/PromptHub Length of output: 13465 🏁 Script executed: file="apps/desktop/src/renderer/components/prompt/PromptListView.tsx"
echo "== Content 1-90 =="
sed -n '1,90p' "$file" | awk '{printf "%4d: %s\n", NR, $0}'
echo "== Content 90-140 =="
sed -n '90,140p' "$file" | awk '{printf "%4d: %s\n", NR+90, $0}'
echo "== Content 140-210 =="
sed -n '140,210p' "$file" | awk '{printf "%4d: %s\n", NR+140, $0}'
echo "== Content 210-260 =="
sed -n '210,260p' "$file" | awk '{printf "%4d: %s\n", NR+210, $0}'
echo "== Search keyboard handlers in file =="
rg -n "onKeyDown|onKeyUp|keydown|Enter|Space" "$file" -S
echo "== Search for onMovePrompt occurrences in apps/desktop/src =="
rg -n "onMovePrompt" apps/desktop/src -SRepository: legeling/PromptHub Length of output: 13373 🏁 Script executed: file="apps/desktop/src/renderer/components/layout/MainContent.tsx"
rg -n "movePrompt" "$file" -SRepository: legeling/PromptHub Length of output: 177 🏁 Script executed: rg -n "movePrompt" apps/desktop/src/renderer -SRepository: legeling/PromptHub Length of output: 1357 🏁 Script executed: sed -n '1,120p' apps/desktop/src/renderer/stores/prompt.store.ts | awk '{print NR ":" $0}'
sed -n '200,280p' apps/desktop/src/renderer/stores/prompt.store.ts | awk '{print NR ":" $0}'
sed -n '300,420p' apps/desktop/src/renderer/services/database.ts | awk '{print NR ":" $0}'Repository: legeling/PromptHub Length of output: 10013 🏁 Script executed: sed -n '1,120p' apps/desktop/src/renderer/stores/prompt.store.ts | awk '{print NR ":" $0}'
sed -n '200,280p' apps/desktop/src/renderer/stores/prompt.store.ts | awk '{print NR ":" $0}'
sed -n '300,420p' apps/desktop/src/renderer/services/database.ts | awk '{print NR ":" $0}'Repository: legeling/PromptHub Length of output: 10013
Also applies to: onMovePrompt: (promptId: string, newParentId: string | null, newOrder: number) => void;🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export function PromptListView({ | ||||||||||||||||||||||
| prompts, | ||||||||||||||||||||||
| selectedId, | ||||||||||||||||||||||
| selectedIds, | ||||||||||||||||||||||
| onSelect, | ||||||||||||||||||||||
| onToggleFavorite, | ||||||||||||||||||||||
| onCopy, | ||||||||||||||||||||||
| onContextMenu, | ||||||||||||||||||||||
| onMovePrompt, | ||||||||||||||||||||||
| }: PromptListViewProps) { | ||||||||||||||||||||||
|
Comment on lines
19
to
28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
ls -la
# show file existence and relevant lines
FILE="apps/desktop/src/renderer/components/prompt/PromptListView.tsx"
if [ -f "$FILE" ]; then
echo "FOUND: $FILE"
nl -ba "$FILE" | sed -n '1,120p'
else
echo "MISSING: $FILE"
fd -a "PromptListView.tsx" .
fiRepository: legeling/PromptHub Length of output: 1563 🏁 Script executed: #!/bin/bash
set -euo pipefail
ls -la
# show file existence and relevant lines
FILE="apps/desktop/src/renderer/components/prompt/PromptListView.tsx"
if [ -f "$FILE" ]; then
echo "FOUND: $FILE"
nl -ba "$FILE" | sed -n '1,160p'
else
echo "MISSING: $FILE"
fd -a "PromptListView.tsx" .
fiRepository: legeling/PromptHub Length of output: 1563 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE="apps/desktop/src/renderer/components/prompt/PromptListView.tsx"
echo "FOUND? $(test -f "$FILE" && echo yes || echo no): $FILE"
echo "---- signature area ----"
python3 - <<'PY'
import itertools,sys
path="apps/desktop/src/renderer/components/prompt/PromptListView.tsx"
start=1
end=120
with open(path,'r',encoding='utf-8') as f:
for i,line in enumerate(f,1):
if start<=i<=end:
print(f"{i:5d}\t{line.rstrip()}")
if i>end:
break
PY
echo "---- exact match for export function PromptListView ----"
rg -n --fixed-string "export function PromptListView" "$FILE" || trueRepository: legeling/PromptHub Length of output: 5818 为导出的
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||||||||||||||
| const { t } = useTranslation(); | ||||||||||||||||||||||
| const [draggingId, setDraggingId] = useState<string | null>(null); | ||||||||||||||||||||||
| const [dropTargetId, setDropTargetId] = useState<string | null>(null); | ||||||||||||||||||||||
| const [dropPosition, setDropPosition] = useState<'before' | 'after' | 'inside' | null>(null); | ||||||||||||||||||||||
| const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set()); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||
| const handleKeyDown = (e: KeyboardEvent) => { | ||||||||||||||||||||||
| if (!selectedId || draggingId) return; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const selectedPrompt = prompts.find(p => p.id === selectedId); | ||||||||||||||||||||||
| if (!selectedPrompt) return; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (e.key === 'Tab' && !e.ctrlKey && !e.metaKey) { | ||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (e.shiftKey) { | ||||||||||||||||||||||
| // Shift+Tab: outdent (move to parent level) | ||||||||||||||||||||||
| if (selectedPrompt.parentId) { | ||||||||||||||||||||||
| const siblings = prompts.filter(p => p.parentId === selectedPrompt.parentId); | ||||||||||||||||||||||
| const parentPrompt = prompts.find(p => p.id === selectedPrompt.parentId); | ||||||||||||||||||||||
| const newOrder = parentPrompt ? prompts.filter(p => p.parentId === parentPrompt?.parentId).length : prompts.filter(p => p.parentId === null).length; | ||||||||||||||||||||||
| onMovePrompt(selectedId, parentPrompt?.parentId || null, newOrder); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } else { | ||||||||||||||||||||||
| // Tab: indent (move to previous sibling's child) | ||||||||||||||||||||||
| const siblings = prompts.filter(p => p.parentId === selectedPrompt.parentId).sort((a, b) => (a.order || 0) - (b.order || 0)); | ||||||||||||||||||||||
| const currentIndex = siblings.findIndex(s => s.id === selectedId); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (currentIndex > 0) { | ||||||||||||||||||||||
| const prevSibling = siblings[currentIndex - 1]; | ||||||||||||||||||||||
| const childCount = prompts.filter(p => p.parentId === prevSibling.id).length; | ||||||||||||||||||||||
| onMovePrompt(selectedId, prevSibling.id, childCount); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| window.addEventListener('keydown', handleKeyDown); | ||||||||||||||||||||||
| return () => window.removeEventListener('keydown', handleKeyDown); | ||||||||||||||||||||||
| }, [selectedId, prompts, onMovePrompt, draggingId]); | ||||||||||||||||||||||
|
Comment on lines
+35
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 9. Tab key hijacked globally PromptListView attaches a window-level keydown listener that prevents default on Tab whenever a prompt is selected, breaking standard Tab focus navigation across the entire app (including inputs) even when the list isn’t focused. Agent Prompt
Comment on lines
+35
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 全局 Line 42-44 对 🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Format date | ||||||||||||||||||||||
| // 格式化日期 | ||||||||||||||||||||||
| const formatDate = (dateStr: string) => { | ||||||||||||||||||||||
| const date = new Date(dateStr); | ||||||||||||||||||||||
| const now = new Date(); | ||||||||||||||||||||||
|
|
@@ -40,29 +85,169 @@ export function PromptListView({ | |||||||||||||||||||||
| } | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return ( | ||||||||||||||||||||||
| <div className="flex flex-col"> | ||||||||||||||||||||||
| {prompts.map((prompt) => ( | ||||||||||||||||||||||
| const toggleExpand = useCallback((promptId: string) => { | ||||||||||||||||||||||
| setExpandedIds(prev => { | ||||||||||||||||||||||
| const newSet = new Set(prev); | ||||||||||||||||||||||
| if (newSet.has(promptId)) { | ||||||||||||||||||||||
| newSet.delete(promptId); | ||||||||||||||||||||||
| } else { | ||||||||||||||||||||||
| newSet.add(promptId); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| return newSet; | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const hasChildren = useCallback((promptId: string) => { | ||||||||||||||||||||||
| return prompts.some(p => p.parentId === promptId); | ||||||||||||||||||||||
| }, [prompts]); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const getChildren = useCallback((parentId: string | null) => { | ||||||||||||||||||||||
| return prompts | ||||||||||||||||||||||
| .filter(p => p.parentId === parentId) | ||||||||||||||||||||||
| .sort((a, b) => (a.order || 0) - (b.order || 0)); | ||||||||||||||||||||||
| }, [prompts]); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const handleDragStart = useCallback((e: React.DragEvent, promptId: string) => { | ||||||||||||||||||||||
| setDraggingId(promptId); | ||||||||||||||||||||||
| e.dataTransfer.effectAllowed = 'move'; | ||||||||||||||||||||||
| e.dataTransfer.setData('text/plain', promptId); | ||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const handleDragEnd = useCallback(() => { | ||||||||||||||||||||||
| setDraggingId(null); | ||||||||||||||||||||||
| setDropTargetId(null); | ||||||||||||||||||||||
| setDropPosition(null); | ||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const handleDragOver = useCallback((e: React.DragEvent) => { | ||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||
| e.dataTransfer.dropEffect = 'move'; | ||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const handleDragEnter = useCallback((e: React.DragEvent, promptId: string) => { | ||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||
| if (draggingId !== promptId) { | ||||||||||||||||||||||
| setDropTargetId(promptId); | ||||||||||||||||||||||
| const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); | ||||||||||||||||||||||
| const y = e.clientY - rect.top; | ||||||||||||||||||||||
| const height = rect.height; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (y < height / 3) { | ||||||||||||||||||||||
| setDropPosition('before'); | ||||||||||||||||||||||
| } else if (y > height * 2 / 3) { | ||||||||||||||||||||||
| setDropPosition('after'); | ||||||||||||||||||||||
| } else { | ||||||||||||||||||||||
| setDropPosition('inside'); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }, [draggingId]); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const handleDragLeave = useCallback((e: React.DragEvent) => { | ||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||
| setDropTargetId(null); | ||||||||||||||||||||||
| setDropPosition(null); | ||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const handleDrop = useCallback((e: React.DragEvent, targetPromptId: string) => { | ||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||
| if (draggingId && draggingId !== targetPromptId) { | ||||||||||||||||||||||
| const targetPrompt = prompts.find(p => p.id === targetPromptId); | ||||||||||||||||||||||
| const draggingPrompt = prompts.find(p => p.id === draggingId); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (targetPrompt && draggingPrompt) { | ||||||||||||||||||||||
| if (dropPosition === 'inside') { | ||||||||||||||||||||||
| const childCount = prompts.filter(p => p.parentId === targetPromptId).length; | ||||||||||||||||||||||
| onMovePrompt(draggingId, targetPromptId, childCount); | ||||||||||||||||||||||
| } else { | ||||||||||||||||||||||
| const newParentId = targetPrompt.parentId; | ||||||||||||||||||||||
| let targetOrder = targetPrompt.order || 0; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (dropPosition === 'after') { | ||||||||||||||||||||||
| targetOrder += 1; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (draggingPrompt.parentId === newParentId && draggingPrompt.order && draggingPrompt.order < targetOrder) { | ||||||||||||||||||||||
| targetOrder -= 1; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+169
to
+171
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 同级重排时 Line 169 使用 建议修复- if (draggingPrompt.parentId === newParentId && draggingPrompt.order && draggingPrompt.order < targetOrder) {
+ if (
+ draggingPrompt.parentId === newParentId &&
+ draggingPrompt.order !== undefined &&
+ draggingPrompt.order < targetOrder
+ ) {
targetOrder -= 1;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| onMovePrompt(draggingId, newParentId, targetOrder); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+151
to
+174
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 8. Cycle can crash tree PromptListView allows moving a prompt inside any other prompt without preventing drops onto descendants, enabling parentId cycles; the recursive renderer has no cycle detection and can recurse indefinitely when expanded. Agent Prompt
|
||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| setDraggingId(null); | ||||||||||||||||||||||
| setDropTargetId(null); | ||||||||||||||||||||||
| setDropPosition(null); | ||||||||||||||||||||||
| }, [draggingId, dropPosition, prompts, onMovePrompt]); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const isSelected = useCallback((promptId: string) => { | ||||||||||||||||||||||
| return selectedId === promptId || selectedIds.includes(promptId); | ||||||||||||||||||||||
| }, [selectedId, selectedIds]); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const isDragging = useCallback((promptId: string) => { | ||||||||||||||||||||||
| return draggingId === promptId; | ||||||||||||||||||||||
| }, [draggingId]); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const isDropTarget = useCallback((promptId: string) => { | ||||||||||||||||||||||
| return dropTargetId === promptId; | ||||||||||||||||||||||
| }, [dropTargetId]); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const renderTreeNode = useCallback((prompt: Prompt, depth: number) => { | ||||||||||||||||||||||
| const hasKids = hasChildren(prompt.id); | ||||||||||||||||||||||
| const isExpanded = expandedIds.has(prompt.id); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return ( | ||||||||||||||||||||||
| <div key={prompt.id}> | ||||||||||||||||||||||
| <div | ||||||||||||||||||||||
| key={prompt.id} | ||||||||||||||||||||||
| draggable | ||||||||||||||||||||||
| onDragStart={(e) => handleDragStart(e, prompt.id)} | ||||||||||||||||||||||
| onDragEnd={handleDragEnd} | ||||||||||||||||||||||
| onDragOver={handleDragOver} | ||||||||||||||||||||||
| onDragEnter={(e) => handleDragEnter(e, prompt.id)} | ||||||||||||||||||||||
| onDragLeave={handleDragLeave} | ||||||||||||||||||||||
| onDrop={(e) => handleDrop(e, prompt.id)} | ||||||||||||||||||||||
| onClick={() => onSelect(prompt.id)} | ||||||||||||||||||||||
| onContextMenu={(e) => onContextMenu(e, prompt)} | ||||||||||||||||||||||
| className={` | ||||||||||||||||||||||
| flex items-center gap-3 px-3 py-2.5 border-b border-border/50 cursor-pointer | ||||||||||||||||||||||
| transition-colors duration-quick | ||||||||||||||||||||||
| ${selectedId === prompt.id | ||||||||||||||||||||||
| transition-colors duration-quick relative | ||||||||||||||||||||||
| ${isSelected(prompt.id) | ||||||||||||||||||||||
| ? 'bg-primary/10 border-l-2 border-l-primary' | ||||||||||||||||||||||
| : 'hover:bg-accent/50' | ||||||||||||||||||||||
| : isDropTarget(prompt.id) && dropPosition === 'inside' | ||||||||||||||||||||||
| ? 'bg-primary/20 border-l-2 border-l-primary' | ||||||||||||||||||||||
| : 'hover:bg-accent/50' | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| ${isDragging(prompt.id) ? 'opacity-50' : ''} | ||||||||||||||||||||||
| ${isDropTarget(prompt.id) && dropPosition === 'inside' ? 'ring-2 ring-primary/50 ring-inset' : ''} | ||||||||||||||||||||||
| ${isDropTarget(prompt.id) && dropPosition === 'before' ? 'border-t-2 border-t-primary' : ''} | ||||||||||||||||||||||
| ${isDropTarget(prompt.id) && dropPosition === 'after' ? 'border-b-2 border-b-primary' : ''} | ||||||||||||||||||||||
| `} | ||||||||||||||||||||||
| style={{ paddingLeft: `${depth * 16 + 12}px` }} | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2. Inline style paddingleft used PromptListView uses an inline style prop to compute indentation, violating the Tailwind-only styling requirement. Inline styles can bypass design tokens and theming conventions (light/dark). Agent Prompt
|
||||||||||||||||||||||
| > | ||||||||||||||||||||||
| {/* Title and description */} | ||||||||||||||||||||||
| {/* 标题和描述 */} | ||||||||||||||||||||||
| <div className="flex items-center gap-1"> | ||||||||||||||||||||||
| {hasKids && ( | ||||||||||||||||||||||
| <button | ||||||||||||||||||||||
| onClick={(e) => { | ||||||||||||||||||||||
| e.stopPropagation(); | ||||||||||||||||||||||
| toggleExpand(prompt.id); | ||||||||||||||||||||||
| }} | ||||||||||||||||||||||
| className="p-0.5 rounded hover:bg-accent transition-colors" | ||||||||||||||||||||||
| > | ||||||||||||||||||||||
| {isExpanded ? ( | ||||||||||||||||||||||
| <ChevronDown className="w-4 h-4 text-muted-foreground" /> | ||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||
| <ChevronRight className="w-4 h-4 text-muted-foreground" /> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
| </button> | ||||||||||||||||||||||
|
Comment on lines
+228
to
+240
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 4. Expand button lacks aria-label The expand/collapse icon-only <button> in PromptListView has no accessible name (no aria-label), which harms screen-reader usability. This violates the accessibility requirement for interactive elements. Agent Prompt
|
||||||||||||||||||||||
| )} | ||||||||||||||||||||||
| {!hasKids && <span className="w-5" />} | ||||||||||||||||||||||
| <GripVertical className="w-4 h-4 text-muted-foreground cursor-grab opacity-0 hover:opacity-100 transition-opacity" /> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| <div className="flex-1 min-w-0"> | ||||||||||||||||||||||
| <div className="flex items-center gap-2"> | ||||||||||||||||||||||
| <h3 | ||||||||||||||||||||||
| className={`font-medium text-sm leading-snug break-words line-clamp-2 ${ | ||||||||||||||||||||||
| selectedId === prompt.id ? 'text-primary' : 'text-foreground' | ||||||||||||||||||||||
| isSelected(prompt.id) ? 'text-primary' : 'text-foreground' | ||||||||||||||||||||||
| }`} | ||||||||||||||||||||||
| title={prompt.title} | ||||||||||||||||||||||
| > | ||||||||||||||||||||||
|
|
@@ -85,24 +270,18 @@ export function PromptListView({ | |||||||||||||||||||||
| )} | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {/* Usage count */} | ||||||||||||||||||||||
| {/* 使用次数 */} | ||||||||||||||||||||||
| <div className="flex-shrink-0 w-12 text-center"> | ||||||||||||||||||||||
| <span className="text-xs text-muted-foreground"> | ||||||||||||||||||||||
| {prompt.usageCount || 0} | ||||||||||||||||||||||
| </span> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {/* Update time */} | ||||||||||||||||||||||
| {/* 更新时间 */} | ||||||||||||||||||||||
| <div className="flex-shrink-0 w-16 text-right"> | ||||||||||||||||||||||
| <span className="text-xs text-muted-foreground"> | ||||||||||||||||||||||
| {formatDate(prompt.updatedAt)} | ||||||||||||||||||||||
| </span> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {/* Action buttons */} | ||||||||||||||||||||||
| {/* 操作按钮 */} | ||||||||||||||||||||||
| <div className="flex items-center gap-1 flex-shrink-0"> | ||||||||||||||||||||||
| <button | ||||||||||||||||||||||
| onClick={(e) => { | ||||||||||||||||||||||
|
|
@@ -129,7 +308,21 @@ export function PromptListView({ | |||||||||||||||||||||
| </button> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| ))} | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {hasKids && isExpanded && ( | ||||||||||||||||||||||
| <div> | ||||||||||||||||||||||
| {getChildren(prompt.id).map((child) => renderTreeNode(child, depth + 1))} | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| }, [hasChildren, expandedIds, toggleExpand, getChildren, isSelected, isDragging, isDropTarget, dropPosition, onSelect, onContextMenu, handleDragStart, handleDragEnd, handleDragOver, handleDragEnter, handleDragLeave, handleDrop, onCopy, onToggleFavorite, t]); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const rootNodes = getChildren(null); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return ( | ||||||||||||||||||||||
| <div className="flex flex-col"> | ||||||||||||||||||||||
| {rootNodes.map((node) => renderTreeNode(node, 0))} | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
7. Workspace hierarchy not persisted
🐞 Bug≡ CorrectnessAgent Prompt
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools