@geminilight/mindos 0.6.29 → 0.6.31
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/README.md +10 -4
- package/README_zh.md +10 -4
- package/app/app/api/acp/config/route.ts +82 -0
- package/app/app/api/acp/detect/route.ts +71 -48
- package/app/app/api/acp/install/route.ts +51 -0
- package/app/app/api/acp/session/route.ts +141 -11
- package/app/app/api/ask/route.ts +126 -18
- package/app/app/api/export/route.ts +105 -0
- package/app/app/api/workflows/route.ts +156 -0
- package/app/app/globals.css +2 -2
- package/app/app/page.tsx +7 -2
- package/app/app/trash/page.tsx +7 -0
- package/app/app/view/[...path]/ViewPageClient.tsx +234 -2
- package/app/components/ActivityBar.tsx +12 -4
- package/app/components/AskModal.tsx +4 -1
- package/app/components/ExportModal.tsx +220 -0
- package/app/components/FileTree.tsx +42 -11
- package/app/components/HomeContent.tsx +92 -20
- package/app/components/MarkdownView.tsx +45 -10
- package/app/components/Panel.tsx +1 -0
- package/app/components/RightAskPanel.tsx +5 -1
- package/app/components/Sidebar.tsx +10 -1
- package/app/components/SidebarLayout.tsx +6 -0
- package/app/components/TrashPageClient.tsx +263 -0
- package/app/components/agents/AgentDetailContent.tsx +263 -47
- package/app/components/agents/AgentsContentPage.tsx +11 -0
- package/app/components/agents/AgentsPanelA2aTab.tsx +285 -46
- package/app/components/agents/AgentsPanelSessionsTab.tsx +166 -0
- package/app/components/agents/agents-content-model.ts +2 -2
- package/app/components/ask/AgentSelectorCapsule.tsx +218 -0
- package/app/components/ask/AskContent.tsx +197 -239
- package/app/components/ask/FileChip.tsx +82 -17
- package/app/components/ask/MentionPopover.tsx +21 -3
- package/app/components/ask/MessageList.tsx +30 -9
- package/app/components/ask/SlashCommandPopover.tsx +21 -3
- package/app/components/ask/ToolCallBlock.tsx +102 -18
- package/app/components/changes/ChangesContentPage.tsx +58 -14
- package/app/components/explore/ExploreContent.tsx +4 -7
- package/app/components/explore/UseCaseCard.tsx +18 -1
- package/app/components/explore/use-cases.generated.ts +76 -0
- package/app/components/explore/use-cases.yaml +185 -0
- package/app/components/panels/AgentsPanel.tsx +1 -0
- package/app/components/panels/AgentsPanelHubNav.tsx +9 -2
- package/app/components/panels/DiscoverPanel.tsx +1 -1
- package/app/components/panels/WorkflowsPanel.tsx +206 -0
- package/app/components/renderers/workflow-yaml/StepEditor.tsx +164 -0
- package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +211 -0
- package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +269 -0
- package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +126 -0
- package/app/components/renderers/workflow-yaml/execution.ts +229 -0
- package/app/components/renderers/workflow-yaml/index.ts +6 -0
- package/app/components/renderers/workflow-yaml/manifest.ts +21 -0
- package/app/components/renderers/workflow-yaml/parser.ts +172 -0
- package/app/components/renderers/workflow-yaml/selectors.tsx +574 -0
- package/app/components/renderers/workflow-yaml/serializer.ts +56 -0
- package/app/components/renderers/workflow-yaml/types.ts +46 -0
- package/app/components/settings/AiTab.tsx +191 -174
- package/app/components/settings/AppearanceTab.tsx +168 -77
- package/app/components/settings/KnowledgeTab.tsx +131 -136
- package/app/components/settings/McpTab.tsx +11 -11
- package/app/components/settings/Primitives.tsx +60 -0
- package/app/components/settings/SettingsContent.tsx +15 -8
- package/app/components/settings/SyncTab.tsx +12 -12
- package/app/components/settings/UninstallTab.tsx +8 -18
- package/app/components/settings/UpdateTab.tsx +82 -82
- package/app/components/settings/types.ts +17 -8
- package/app/hooks/useAcpConfig.ts +96 -0
- package/app/hooks/useAcpDetection.ts +69 -14
- package/app/hooks/useAcpRegistry.ts +46 -11
- package/app/hooks/useAskModal.ts +12 -5
- package/app/hooks/useAskPanel.ts +8 -5
- package/app/hooks/useAskSession.ts +19 -2
- package/app/hooks/useImageUpload.ts +152 -0
- package/app/lib/acp/acp-tools.ts +3 -1
- package/app/lib/acp/agent-descriptors.ts +274 -0
- package/app/lib/acp/bridge.ts +6 -0
- package/app/lib/acp/index.ts +20 -4
- package/app/lib/acp/registry.ts +74 -7
- package/app/lib/acp/session.ts +490 -28
- package/app/lib/acp/subprocess.ts +307 -21
- package/app/lib/acp/types.ts +158 -20
- package/app/lib/actions.ts +57 -3
- package/app/lib/agent/model.ts +18 -3
- package/app/lib/agent/stream-consumer.ts +18 -0
- package/app/lib/agent/to-agent-messages.ts +25 -2
- package/app/lib/agent/tools.ts +56 -9
- package/app/lib/core/export.ts +116 -0
- package/app/lib/core/trash.ts +241 -0
- package/app/lib/fs.ts +47 -0
- package/app/lib/hooks/usePinnedFiles.ts +90 -0
- package/app/lib/i18n/generated/explore-i18n.generated.ts +138 -0
- package/app/lib/i18n/index.ts +3 -0
- package/app/lib/i18n/modules/knowledge.ts +124 -6
- package/app/lib/i18n/modules/navigation.ts +2 -0
- package/app/lib/i18n/modules/onboarding.ts +2 -134
- package/app/lib/i18n/modules/panels.ts +146 -2
- package/app/lib/i18n/modules/settings.ts +12 -0
- package/app/lib/pi-integration/skills.ts +21 -6
- package/app/lib/renderers/index.ts +2 -2
- package/app/lib/settings.ts +10 -0
- package/app/lib/types.ts +12 -1
- package/app/next-env.d.ts +1 -1
- package/app/package.json +11 -3
- package/app/scripts/generate-explore.ts +145 -0
- package/package.json +1 -1
- package/templates/en/.mindos/workflows/Sprint Release.flow.yaml +130 -0
- package/templates/zh/.mindos/workflows//345/221/250/350/277/255/344/273/243/346/243/200/346/237/245.flow.yaml +84 -0
- package/app/components/explore/use-cases.ts +0 -58
- package/app/components/renderers/workflow/WorkflowRenderer.tsx +0 -409
- package/app/components/renderers/workflow/manifest.ts +0 -14
|
@@ -1,31 +1,96 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useState, useRef, useEffect } from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
import { X, FileText, Table, Paperclip, ImageIcon, Zap, Bot } from 'lucide-react';
|
|
4
6
|
|
|
5
7
|
interface FileChipProps {
|
|
6
8
|
path: string;
|
|
7
9
|
onRemove: () => void;
|
|
8
|
-
variant?: 'kb' | 'upload';
|
|
10
|
+
variant?: 'kb' | 'upload' | 'image' | 'skill' | 'agent';
|
|
11
|
+
/** Base64 image data for hover preview (variant='image' only) */
|
|
12
|
+
imageData?: string;
|
|
13
|
+
/** MIME type for image preview */
|
|
14
|
+
imageMime?: string;
|
|
9
15
|
}
|
|
10
16
|
|
|
11
|
-
|
|
17
|
+
const VARIANT_ICON = {
|
|
18
|
+
kb: { icon: FileText, cls: 'text-muted-foreground' },
|
|
19
|
+
upload: { icon: Paperclip, cls: 'text-muted-foreground' },
|
|
20
|
+
image: { icon: ImageIcon, cls: 'text-muted-foreground' },
|
|
21
|
+
skill: { icon: Zap, cls: 'text-[var(--amber)]' },
|
|
22
|
+
agent: { icon: Bot, cls: 'text-muted-foreground' },
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
const VARIANT_STYLE = {
|
|
26
|
+
kb: 'border-border bg-muted text-foreground',
|
|
27
|
+
upload: 'border-border bg-muted text-foreground',
|
|
28
|
+
image: 'border-border bg-muted text-foreground',
|
|
29
|
+
skill: 'border-[var(--amber)]/25 bg-[var(--amber)]/10 text-foreground',
|
|
30
|
+
agent: 'border-[var(--amber)]/25 bg-[var(--amber)]/10 text-foreground',
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
export default function FileChip({ path, onRemove, variant = 'kb', imageData, imageMime }: FileChipProps) {
|
|
34
|
+
const [showPreview, setShowPreview] = useState(false);
|
|
35
|
+
const [mounted, setMounted] = useState(false);
|
|
36
|
+
const chipRef = useRef<HTMLSpanElement>(null);
|
|
37
|
+
const [previewPos, setPreviewPos] = useState<{ top: number; left: number } | null>(null);
|
|
38
|
+
|
|
39
|
+
useEffect(() => setMounted(true), []);
|
|
40
|
+
|
|
12
41
|
const name = path.split('/').pop() ?? path;
|
|
13
|
-
const isCsv = name.endsWith('.csv');
|
|
14
|
-
const
|
|
15
|
-
|
|
42
|
+
const isCsv = variant === 'kb' && name.endsWith('.csv');
|
|
43
|
+
const { icon: Icon, cls: iconClass } = isCsv
|
|
44
|
+
? { icon: Table, cls: 'text-success' }
|
|
45
|
+
: VARIANT_ICON[variant];
|
|
46
|
+
const style = VARIANT_STYLE[variant];
|
|
47
|
+
|
|
48
|
+
// Position preview above the chip using getBoundingClientRect
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!showPreview || !chipRef.current) { setPreviewPos(null); return; }
|
|
51
|
+
const rect = chipRef.current.getBoundingClientRect();
|
|
52
|
+
setPreviewPos({
|
|
53
|
+
top: rect.top - 8, // 8px gap above chip
|
|
54
|
+
left: rect.left,
|
|
55
|
+
});
|
|
56
|
+
}, [showPreview]);
|
|
57
|
+
|
|
58
|
+
const preview = showPreview && previewPos && imageData && imageMime ? (
|
|
59
|
+
<div
|
|
60
|
+
className="fixed z-50 p-1 rounded-lg border border-border bg-card shadow-lg pointer-events-none"
|
|
61
|
+
style={{
|
|
62
|
+
left: previewPos.left,
|
|
63
|
+
bottom: window.innerHeight - previewPos.top,
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
<img
|
|
67
|
+
src={`data:${imageMime};base64,${imageData}`}
|
|
68
|
+
alt={name}
|
|
69
|
+
className="max-h-48 max-w-64 rounded object-contain"
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
) : null;
|
|
16
73
|
|
|
17
74
|
return (
|
|
18
|
-
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
aria-label={`Remove ${name}`}
|
|
25
|
-
className="p-1 -mr-1 rounded hover:bg-muted hover:text-foreground transition-colors shrink-0"
|
|
75
|
+
<>
|
|
76
|
+
<span
|
|
77
|
+
ref={chipRef}
|
|
78
|
+
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs border max-w-[220px] ${style}`}
|
|
79
|
+
onMouseEnter={() => imageData && setShowPreview(true)}
|
|
80
|
+
onMouseLeave={() => setShowPreview(false)}
|
|
26
81
|
>
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
82
|
+
<Icon size={11} className={`${iconClass} shrink-0`} />
|
|
83
|
+
<span className="truncate" title={path}>{name}</span>
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
onClick={onRemove}
|
|
87
|
+
aria-label={`Remove ${name}`}
|
|
88
|
+
className="p-0.5 -mr-1 rounded hover:text-foreground transition-colors shrink-0 text-muted-foreground"
|
|
89
|
+
>
|
|
90
|
+
<X size={10} />
|
|
91
|
+
</button>
|
|
92
|
+
</span>
|
|
93
|
+
{mounted && preview && createPortal(preview, document.body)}
|
|
94
|
+
</>
|
|
30
95
|
);
|
|
31
96
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { FileText, Table, FolderOpen } from 'lucide-react';
|
|
5
5
|
import HighlightMatch from './HighlightMatch';
|
|
6
6
|
|
|
@@ -11,8 +11,26 @@ interface MentionPopoverProps {
|
|
|
11
11
|
onSelect: (filePath: string) => void;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Popover for @ file mentions. Renders as a flex child within the AskContent
|
|
16
|
+
* column (not absolutely positioned) to avoid clipping by the panel's
|
|
17
|
+
* overflow-hidden. Uses a dynamic max-height that caps at 50% of the
|
|
18
|
+
* viewport so the popover never overflows the visible area.
|
|
19
|
+
*/
|
|
14
20
|
export default function MentionPopover({ results, selectedIndex, query, onSelect }: MentionPopoverProps) {
|
|
15
21
|
const listRef = useRef<HTMLDivElement>(null);
|
|
22
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
23
|
+
const [maxListH, setMaxListH] = useState(280);
|
|
24
|
+
|
|
25
|
+
// Dynamically compute max list height based on available space
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const container = containerRef.current;
|
|
28
|
+
if (!container) return;
|
|
29
|
+
const rect = container.getBoundingClientRect();
|
|
30
|
+
// Reserve ~80px for header + footer chrome, and cap at 50vh
|
|
31
|
+
const available = Math.min(rect.top - 8, window.innerHeight * 0.5) - 80;
|
|
32
|
+
setMaxListH(Math.max(120, Math.min(320, available)));
|
|
33
|
+
}, [results.length]);
|
|
16
34
|
|
|
17
35
|
useEffect(() => {
|
|
18
36
|
const container = listRef.current;
|
|
@@ -24,13 +42,13 @@ export default function MentionPopover({ results, selectedIndex, query, onSelect
|
|
|
24
42
|
if (results.length === 0) return null;
|
|
25
43
|
|
|
26
44
|
return (
|
|
27
|
-
<div className="border border-border rounded-lg bg-card shadow-lg overflow-hidden">
|
|
45
|
+
<div ref={containerRef} className="border border-border rounded-lg bg-card shadow-lg overflow-hidden">
|
|
28
46
|
<div className="px-3 py-1.5 border-b border-border flex items-center gap-1.5">
|
|
29
47
|
<FolderOpen size={11} className="text-muted-foreground/50" />
|
|
30
48
|
<span className="text-2xs font-medium text-muted-foreground/70 uppercase tracking-wider">Files</span>
|
|
31
49
|
<span className="text-2xs text-muted-foreground/40 ml-auto">{results.length}</span>
|
|
32
50
|
</div>
|
|
33
|
-
<div ref={listRef} className="
|
|
51
|
+
<div ref={listRef} className="overflow-y-auto" style={{ maxHeight: maxListH }}>
|
|
34
52
|
{results.map((f, idx) => {
|
|
35
53
|
const name = f.split('/').pop() ?? f;
|
|
36
54
|
const dir = f.split('/').slice(0, -1).join('/');
|
|
@@ -4,25 +4,46 @@ import { useRef, useEffect } from 'react';
|
|
|
4
4
|
import { Sparkles, Loader2, AlertCircle, Wrench, WifiOff, Zap } from 'lucide-react';
|
|
5
5
|
import ReactMarkdown from 'react-markdown';
|
|
6
6
|
import remarkGfm from 'remark-gfm';
|
|
7
|
-
import type { Message } from '@/lib/types';
|
|
7
|
+
import type { Message, ImagePart } from '@/lib/types';
|
|
8
8
|
import { stripThinkingTags } from '@/hooks/useAiOrganize';
|
|
9
9
|
import ToolCallBlock from './ToolCallBlock';
|
|
10
10
|
import ThinkingBlock from './ThinkingBlock';
|
|
11
11
|
|
|
12
12
|
const SKILL_PREFIX_RE = /^Use the skill ([^:]+):\s*/;
|
|
13
13
|
|
|
14
|
-
function UserMessageContent({ content, skillName }: { content: string; skillName?: string }) {
|
|
14
|
+
function UserMessageContent({ content, skillName, images }: { content: string; skillName?: string; images?: ImagePart[] }) {
|
|
15
15
|
const resolved = skillName ?? content.match(SKILL_PREFIX_RE)?.[1];
|
|
16
|
-
if (!resolved) return <>{content}</>;
|
|
17
16
|
const prefixMatch = content.match(SKILL_PREFIX_RE);
|
|
18
17
|
const rest = prefixMatch ? content.slice(prefixMatch[0].length) : content;
|
|
19
18
|
return (
|
|
20
19
|
<>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
{
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
{/* Images */}
|
|
21
|
+
{images && images.length > 0 && (
|
|
22
|
+
<div className={`flex flex-wrap gap-1.5${content ? ' mb-2' : ''}`}>
|
|
23
|
+
{images.map((img, idx) => (
|
|
24
|
+
img.data ? (
|
|
25
|
+
<img
|
|
26
|
+
key={idx}
|
|
27
|
+
src={`data:${img.mimeType};base64,${img.data}`}
|
|
28
|
+
alt={`Image ${idx + 1}`}
|
|
29
|
+
className="max-h-48 max-w-full rounded-md object-contain"
|
|
30
|
+
/>
|
|
31
|
+
) : (
|
|
32
|
+
<div key={idx} className="h-12 px-3 rounded-md bg-muted flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
33
|
+
<span>[Image {idx + 1}]</span>
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
)}
|
|
39
|
+
{/* Skill capsule + text */}
|
|
40
|
+
{resolved && (
|
|
41
|
+
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-[11px] font-medium bg-[var(--amber)]/15 text-[var(--amber)] mr-1 align-middle">
|
|
42
|
+
<Zap size={10} className="shrink-0" />
|
|
43
|
+
{resolved}
|
|
44
|
+
</span>
|
|
45
|
+
)}
|
|
46
|
+
{resolved ? rest : content}
|
|
26
47
|
</>
|
|
27
48
|
);
|
|
28
49
|
}
|
|
@@ -168,7 +189,7 @@ export default function MessageList({
|
|
|
168
189
|
<div
|
|
169
190
|
className="max-w-[85%] px-3 py-2 rounded-xl rounded-br-sm text-sm leading-relaxed whitespace-pre-wrap bg-[var(--amber)] text-[var(--amber-foreground)]"
|
|
170
191
|
>
|
|
171
|
-
<UserMessageContent content={m.content} skillName={m.skillName} />
|
|
192
|
+
<UserMessageContent content={m.content} skillName={m.skillName} images={m.images} />
|
|
172
193
|
</div>
|
|
173
194
|
) : m.content.startsWith('__error__') ? (
|
|
174
195
|
<div className="max-w-[85%] px-3 py-2.5 rounded-xl rounded-bl-sm border border-error/20 bg-error/8 text-sm">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { Zap } from 'lucide-react';
|
|
5
5
|
import type { SlashItem } from '@/hooks/useSlashCommand';
|
|
6
6
|
import HighlightMatch from './HighlightMatch';
|
|
@@ -12,8 +12,26 @@ interface SlashCommandPopoverProps {
|
|
|
12
12
|
onSelect: (item: SlashItem) => void;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Popover for slash commands. Renders as a flex child within the AskContent
|
|
17
|
+
* column (not absolutely positioned) to avoid clipping by the panel's
|
|
18
|
+
* overflow-hidden. Uses a dynamic max-height that caps at 50% of the
|
|
19
|
+
* viewport so the popover never overflows the visible area.
|
|
20
|
+
*/
|
|
15
21
|
export default function SlashCommandPopover({ results, selectedIndex, query, onSelect }: SlashCommandPopoverProps) {
|
|
16
22
|
const listRef = useRef<HTMLDivElement>(null);
|
|
23
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
24
|
+
const [maxListH, setMaxListH] = useState(280);
|
|
25
|
+
|
|
26
|
+
// Dynamically compute max list height based on available space
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const container = containerRef.current;
|
|
29
|
+
if (!container) return;
|
|
30
|
+
const rect = container.getBoundingClientRect();
|
|
31
|
+
// Reserve ~80px for header + footer chrome, and cap at 50vh
|
|
32
|
+
const available = Math.min(rect.top - 8, window.innerHeight * 0.5) - 80;
|
|
33
|
+
setMaxListH(Math.max(120, Math.min(320, available)));
|
|
34
|
+
}, [results.length]);
|
|
17
35
|
|
|
18
36
|
useEffect(() => {
|
|
19
37
|
const container = listRef.current;
|
|
@@ -25,13 +43,13 @@ export default function SlashCommandPopover({ results, selectedIndex, query, onS
|
|
|
25
43
|
if (results.length === 0) return null;
|
|
26
44
|
|
|
27
45
|
return (
|
|
28
|
-
<div className="border border-border rounded-lg bg-card shadow-lg overflow-hidden">
|
|
46
|
+
<div ref={containerRef} className="border border-border rounded-lg bg-card shadow-lg overflow-hidden">
|
|
29
47
|
<div className="px-3 py-1.5 border-b border-border flex items-center gap-1.5">
|
|
30
48
|
<Zap size={11} className="text-[var(--amber)]/50" />
|
|
31
49
|
<span className="text-2xs font-medium text-muted-foreground/70 uppercase tracking-wider">Skills</span>
|
|
32
50
|
<span className="text-2xs text-muted-foreground/40 ml-auto">{results.length}</span>
|
|
33
51
|
</div>
|
|
34
|
-
<div ref={listRef} className="
|
|
52
|
+
<div ref={listRef} className="overflow-y-auto" style={{ maxHeight: maxListH }}>
|
|
35
53
|
{results.map((item, idx) => (
|
|
36
54
|
<button
|
|
37
55
|
key={item.name}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react';
|
|
3
|
+
import { useState, useMemo } from 'react';
|
|
4
4
|
import { ChevronRight, ChevronDown, Loader2, CheckCircle2, XCircle, AlertTriangle } from 'lucide-react';
|
|
5
5
|
import type { ToolCallPart } from '@/lib/types';
|
|
6
6
|
|
|
7
7
|
const DESTRUCTIVE_TOOLS = new Set(['delete_file', 'move_file', 'rename_file', 'write_file']);
|
|
8
8
|
|
|
9
|
+
/** Tools that produce diff output — auto-expand when done */
|
|
10
|
+
const DIFF_TOOLS = new Set([
|
|
11
|
+
'write_file', 'create_file', 'update_section',
|
|
12
|
+
'insert_after_heading', 'edit_lines', 'append_to_file',
|
|
13
|
+
]);
|
|
14
|
+
|
|
9
15
|
const TOOL_ICONS: Record<string, string> = {
|
|
10
16
|
search: '🔍',
|
|
11
17
|
list_files: '📂',
|
|
@@ -41,18 +47,55 @@ function formatInput(input: unknown): string {
|
|
|
41
47
|
return parts.join(', ');
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
const CHANGES_SEPARATOR = '--- changes ---';
|
|
51
|
+
|
|
52
|
+
/** Parse tool output: extract header line (before separator) and diff lines (after separator) */
|
|
53
|
+
function parseToolOutput(output: string | undefined): { header: string; stats: string; diffLines: { prefix: string; text: string }[] } {
|
|
54
|
+
if (!output) return { header: '', stats: '', diffLines: [] };
|
|
55
|
+
const sepIdx = output.indexOf(CHANGES_SEPARATOR);
|
|
56
|
+
if (sepIdx === -1) return { header: output, stats: '', diffLines: [] };
|
|
57
|
+
|
|
58
|
+
const header = output.slice(0, sepIdx).trim();
|
|
59
|
+
const diffText = output.slice(sepIdx + CHANGES_SEPARATOR.length).trim();
|
|
60
|
+
|
|
61
|
+
// Extract stats from header, e.g. "File written: foo.md (+3 −1)"
|
|
62
|
+
const statsMatch = header.match(/\((\+\d+\s*−\d+)\)/);
|
|
63
|
+
const stats = statsMatch ? statsMatch[1] : '';
|
|
64
|
+
|
|
65
|
+
const diffLines = diffText.split('\n').map(line => {
|
|
66
|
+
if (line.startsWith('+ ')) return { prefix: '+', text: line.slice(2) };
|
|
67
|
+
if (line.startsWith('- ')) return { prefix: '-', text: line.slice(2) };
|
|
68
|
+
if (line.startsWith(' ')) return { prefix: ' ', text: line.slice(2) };
|
|
69
|
+
// gap or other
|
|
70
|
+
return { prefix: ' ', text: line };
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return { header, stats, diffLines };
|
|
48
74
|
}
|
|
49
75
|
|
|
50
76
|
export default function ToolCallBlock({ part }: { part: ToolCallPart }) {
|
|
51
|
-
const
|
|
77
|
+
const hasDiff = DIFF_TOOLS.has(part.toolName);
|
|
78
|
+
const isDone = part.state === 'done';
|
|
79
|
+
// Auto-expand diff tools when completed
|
|
80
|
+
const [manualToggle, setManualToggle] = useState<boolean | null>(null);
|
|
81
|
+
const expanded = manualToggle ?? (hasDiff && isDone);
|
|
82
|
+
|
|
52
83
|
const icon = TOOL_ICONS[part.toolName] ?? '🔧';
|
|
53
|
-
const inputSummary = formatInput(part.input);
|
|
54
84
|
const isDestructive = DESTRUCTIVE_TOOLS.has(part.toolName);
|
|
55
85
|
|
|
86
|
+
const parsed = useMemo(() => parseToolOutput(part.output), [part.output]);
|
|
87
|
+
|
|
88
|
+
// For collapsed header: show file path from input + stats
|
|
89
|
+
const filePath = useMemo(() => {
|
|
90
|
+
if (!part.input || typeof part.input !== 'object') return '';
|
|
91
|
+
const obj = part.input as Record<string, unknown>;
|
|
92
|
+
return (obj.path as string) ?? '';
|
|
93
|
+
}, [part.input]);
|
|
94
|
+
|
|
95
|
+
const headerLabel = filePath
|
|
96
|
+
? `${filePath.split('/').pop() ?? filePath}${parsed.stats ? ` (${parsed.stats})` : ''}`
|
|
97
|
+
: formatInput(part.input);
|
|
98
|
+
|
|
56
99
|
return (
|
|
57
100
|
<div className={`my-1 rounded-md border text-xs font-mono ${
|
|
58
101
|
isDestructive
|
|
@@ -61,14 +104,14 @@ export default function ToolCallBlock({ part }: { part: ToolCallPart }) {
|
|
|
61
104
|
}`}>
|
|
62
105
|
<button
|
|
63
106
|
type="button"
|
|
64
|
-
onClick={() =>
|
|
107
|
+
onClick={() => setManualToggle(v => v === null ? !expanded : !v)}
|
|
65
108
|
className="w-full flex items-center gap-1.5 px-2 py-1.5 text-left hover:bg-muted/50 transition-colors rounded-md"
|
|
66
109
|
>
|
|
67
110
|
{expanded ? <ChevronDown size={12} className="shrink-0 text-muted-foreground" /> : <ChevronRight size={12} className="shrink-0 text-muted-foreground" />}
|
|
68
111
|
{isDestructive && <AlertTriangle size={11} className="shrink-0 text-[var(--amber)]" />}
|
|
69
112
|
<span>{icon}</span>
|
|
70
113
|
<span className={`font-medium ${isDestructive ? 'text-[var(--amber)]' : 'text-foreground'}`}>{part.toolName}</span>
|
|
71
|
-
<span className="text-muted-foreground truncate flex-1">
|
|
114
|
+
<span className="text-muted-foreground truncate flex-1">{headerLabel}</span>
|
|
72
115
|
<span className="shrink-0 ml-auto">
|
|
73
116
|
{part.state === 'pending' || part.state === 'running' ? (
|
|
74
117
|
<Loader2 size={12} className="animate-spin text-[var(--amber)]" />
|
|
@@ -80,15 +123,56 @@ export default function ToolCallBlock({ part }: { part: ToolCallPart }) {
|
|
|
80
123
|
</span>
|
|
81
124
|
</button>
|
|
82
125
|
{expanded && (
|
|
83
|
-
<div className="
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
126
|
+
<div className="border-t border-border/30">
|
|
127
|
+
{/* Diff view for file-mutating tools — only when done and has diff */}
|
|
128
|
+
{hasDiff && isDone && parsed.diffLines.length > 0 ? (
|
|
129
|
+
<div className="max-h-64 overflow-y-auto">
|
|
130
|
+
{parsed.diffLines.map((line, idx) => (
|
|
131
|
+
<div
|
|
132
|
+
key={idx}
|
|
133
|
+
className={`px-2 py-px flex items-start gap-1.5 ${
|
|
134
|
+
line.prefix === '+'
|
|
135
|
+
? 'bg-success/8'
|
|
136
|
+
: line.prefix === '-'
|
|
137
|
+
? 'bg-error/8'
|
|
138
|
+
: ''
|
|
139
|
+
}`}
|
|
140
|
+
>
|
|
141
|
+
<span
|
|
142
|
+
className={`select-none w-3 shrink-0 text-right ${
|
|
143
|
+
line.prefix === '+' ? 'text-success' : line.prefix === '-' ? 'text-error' : 'text-muted-foreground/50'
|
|
144
|
+
}`}
|
|
145
|
+
>
|
|
146
|
+
{line.prefix}
|
|
147
|
+
</span>
|
|
148
|
+
<span
|
|
149
|
+
className={`whitespace-pre-wrap break-all flex-1 ${
|
|
150
|
+
line.prefix === '+' ? 'text-success' : line.prefix === '-' ? 'text-error' : 'text-muted-foreground'
|
|
151
|
+
}`}
|
|
152
|
+
>
|
|
153
|
+
{line.text || '\u00A0'}
|
|
154
|
+
</span>
|
|
155
|
+
</div>
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
) : (
|
|
159
|
+
/* Fallback: show input (always), output when available */
|
|
160
|
+
<div className="px-2 pb-2 pt-1 space-y-1">
|
|
161
|
+
{part.state === 'running' && (
|
|
162
|
+
<div className="text-muted-foreground/60 text-2xs flex items-center gap-1">
|
|
163
|
+
<Loader2 size={10} className="animate-spin" /> Running...
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
<div className="text-muted-foreground">
|
|
167
|
+
<span className="font-semibold">Input: </span>
|
|
168
|
+
<span className="break-all whitespace-pre-wrap">{JSON.stringify(part.input, null, 2)}</span>
|
|
169
|
+
</div>
|
|
170
|
+
{part.output !== undefined && part.output !== '' && (
|
|
171
|
+
<div className="text-muted-foreground">
|
|
172
|
+
<span className="font-semibold">Output: </span>
|
|
173
|
+
<span className="break-all whitespace-pre-wrap">{part.output.length > 500 ? part.output.slice(0, 500) + '…' : part.output}</span>
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
92
176
|
</div>
|
|
93
177
|
)}
|
|
94
178
|
</div>
|
|
@@ -221,7 +221,10 @@ export default function ChangesContentPage({ initialPath = '' }: { initialPath?:
|
|
|
221
221
|
|
|
222
222
|
{!loading && !error && events.map((event) => {
|
|
223
223
|
const open = !!expanded[event.id];
|
|
224
|
-
const
|
|
224
|
+
const rawDiff = buildLineDiff(event.before ?? '', event.after ?? '');
|
|
225
|
+
const rows = collapseDiffContext(rawDiff);
|
|
226
|
+
const inserts = rawDiff.filter(r => r.type === 'insert').length;
|
|
227
|
+
const deletes = rawDiff.filter(r => r.type === 'delete').length;
|
|
225
228
|
return (
|
|
226
229
|
<div key={event.id} className="rounded-xl border border-border bg-card overflow-hidden">
|
|
227
230
|
<button
|
|
@@ -232,7 +235,16 @@ export default function ChangesContentPage({ initialPath = '' }: { initialPath?:
|
|
|
232
235
|
<div className="flex items-start gap-2">
|
|
233
236
|
<span className="pt-0.5 text-muted-foreground">{open ? <ChevronDown size={14} /> : <ChevronRight size={14} />}</span>
|
|
234
237
|
<div className="min-w-0 flex-1">
|
|
235
|
-
<div className="
|
|
238
|
+
<div className="flex items-center gap-2">
|
|
239
|
+
<span className="text-sm font-medium text-foreground font-display">{event.summary}</span>
|
|
240
|
+
{(inserts > 0 || deletes > 0) && (
|
|
241
|
+
<span className="text-xs font-mono text-muted-foreground">
|
|
242
|
+
{inserts > 0 && <span className="text-success">+{inserts}</span>}
|
|
243
|
+
{inserts > 0 && deletes > 0 && ' '}
|
|
244
|
+
{deletes > 0 && <span className="text-error">-{deletes}</span>}
|
|
245
|
+
</span>
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
236
248
|
<div className="mt-1 flex flex-wrap items-center gap-1.5 text-xs text-muted-foreground">
|
|
237
249
|
<span
|
|
238
250
|
className="rounded-md px-2 py-0.5 font-medium"
|
|
@@ -261,27 +273,59 @@ export default function ChangesContentPage({ initialPath = '' }: { initialPath?:
|
|
|
261
273
|
</div>
|
|
262
274
|
</button>
|
|
263
275
|
|
|
264
|
-
{open && (
|
|
265
|
-
|
|
276
|
+
{open && (() => {
|
|
277
|
+
let oln = 1;
|
|
278
|
+
let nln = 1;
|
|
279
|
+
return (
|
|
280
|
+
<div className="border-t border-border bg-background/70 max-h-80 overflow-y-auto">
|
|
266
281
|
{rows.map((row, idx) => {
|
|
267
282
|
if (row.type === 'gap') {
|
|
268
|
-
|
|
283
|
+
oln += row.count;
|
|
284
|
+
nln += row.count;
|
|
285
|
+
return (
|
|
286
|
+
<div key={`${event.id}-gap-${idx}`} className="px-3 py-1 text-2xs text-muted-foreground/60 border-y border-border/30 bg-muted/20 text-center">
|
|
287
|
+
{t.changes.unchangedLines(row.count)}
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
269
290
|
}
|
|
270
291
|
const prefix = row.type === 'insert' ? '+' : row.type === 'delete' ? '-' : ' ';
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
: 'var(--muted-foreground)';
|
|
292
|
+
const showOld = row.type !== 'insert' ? oln : '';
|
|
293
|
+
const showNew = row.type !== 'delete' ? nln : '';
|
|
294
|
+
if (row.type !== 'insert') oln++;
|
|
295
|
+
if (row.type !== 'delete') nln++;
|
|
276
296
|
return (
|
|
277
|
-
<div
|
|
278
|
-
|
|
279
|
-
|
|
297
|
+
<div
|
|
298
|
+
key={`${event.id}-${idx}`}
|
|
299
|
+
className={`flex items-start text-xs font-mono ${
|
|
300
|
+
row.type === 'insert'
|
|
301
|
+
? 'bg-success/8'
|
|
302
|
+
: row.type === 'delete'
|
|
303
|
+
? 'bg-error/8'
|
|
304
|
+
: ''
|
|
305
|
+
}`}
|
|
306
|
+
>
|
|
307
|
+
<span className="w-8 shrink-0 text-right pr-1 select-none text-muted-foreground/40 text-2xs leading-5">{showOld}</span>
|
|
308
|
+
<span className="w-8 shrink-0 text-right pr-1 select-none text-muted-foreground/40 text-2xs leading-5">{showNew}</span>
|
|
309
|
+
<span
|
|
310
|
+
className={`w-3 shrink-0 text-center select-none leading-5 ${
|
|
311
|
+
row.type === 'insert' ? 'text-success' : row.type === 'delete' ? 'text-error' : 'text-muted-foreground/30'
|
|
312
|
+
}`}
|
|
313
|
+
>
|
|
314
|
+
{prefix}
|
|
315
|
+
</span>
|
|
316
|
+
<span
|
|
317
|
+
className={`px-1 py-0.5 whitespace-pre-wrap break-all flex-1 ${
|
|
318
|
+
row.type === 'insert' ? 'text-success' : row.type === 'delete' ? 'text-error' : 'text-muted-foreground'
|
|
319
|
+
}`}
|
|
320
|
+
>
|
|
321
|
+
{row.text || '\u00A0'}
|
|
322
|
+
</span>
|
|
280
323
|
</div>
|
|
281
324
|
);
|
|
282
325
|
})}
|
|
283
326
|
</div>
|
|
284
|
-
|
|
327
|
+
);
|
|
328
|
+
})()}
|
|
285
329
|
</div>
|
|
286
330
|
);
|
|
287
331
|
})}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
4
|
import { useLocale } from '@/lib/LocaleContext';
|
|
5
|
-
import { useCases, categories, scenarios, type UseCaseCategory, type UseCaseScenario } from './use-cases';
|
|
5
|
+
import { useCases, categories, scenarios, type UseCaseCategory, type UseCaseScenario } from './use-cases.generated';
|
|
6
6
|
import UseCaseCard from './UseCaseCard';
|
|
7
7
|
|
|
8
8
|
export default function ExploreContent() {
|
|
@@ -17,13 +17,9 @@ export default function ExploreContent() {
|
|
|
17
17
|
return true;
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
/**
|
|
20
|
+
/** Dynamic lookup for use case i18n data by id (works for any number of cases) */
|
|
21
21
|
const getUseCaseText = (id: string): { title: string; desc: string; prompt: string } | undefined => {
|
|
22
|
-
|
|
23
|
-
c1: e.c1, c2: e.c2, c3: e.c3, c4: e.c4, c5: e.c5,
|
|
24
|
-
c6: e.c6, c7: e.c7, c8: e.c8, c9: e.c9,
|
|
25
|
-
};
|
|
26
|
-
return map[id];
|
|
22
|
+
return (e as Record<string, any>)[id] as { title: string; desc: string; prompt: string } | undefined;
|
|
27
23
|
};
|
|
28
24
|
|
|
29
25
|
return (
|
|
@@ -90,6 +86,7 @@ export default function ExploreContent() {
|
|
|
90
86
|
<UseCaseCard
|
|
91
87
|
key={uc.id}
|
|
92
88
|
icon={uc.icon}
|
|
89
|
+
image={uc.image}
|
|
93
90
|
title={data.title}
|
|
94
91
|
description={data.desc}
|
|
95
92
|
prompt={data.prompt}
|
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { useState } from 'react';
|
|
3
4
|
import { openAskModal } from '@/hooks/useAskModal';
|
|
4
5
|
|
|
5
6
|
interface UseCaseCardProps {
|
|
6
7
|
icon: string;
|
|
8
|
+
image?: string;
|
|
7
9
|
title: string;
|
|
8
10
|
description: string;
|
|
9
11
|
prompt: string;
|
|
10
12
|
tryItLabel: string;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
export default function UseCaseCard({ icon, title, description, prompt, tryItLabel }: UseCaseCardProps) {
|
|
15
|
+
export default function UseCaseCard({ icon, image, title, description, prompt, tryItLabel }: UseCaseCardProps) {
|
|
16
|
+
const [imgError, setImgError] = useState(false);
|
|
17
|
+
|
|
14
18
|
return (
|
|
15
19
|
<div
|
|
16
20
|
className="group flex flex-col gap-3 p-4 rounded-xl border border-border bg-card transition-all duration-150 hover:border-[var(--amber)]/30 hover:bg-muted/50"
|
|
17
21
|
>
|
|
22
|
+
{/* Image or emoji fallback */}
|
|
23
|
+
{image && !imgError ? (
|
|
24
|
+
<div className="w-full aspect-[16/9] rounded-lg overflow-hidden bg-muted">
|
|
25
|
+
<img
|
|
26
|
+
src={image}
|
|
27
|
+
alt={title}
|
|
28
|
+
className="w-full h-full object-cover"
|
|
29
|
+
loading="lazy"
|
|
30
|
+
onError={() => setImgError(true)}
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
) : null}
|
|
34
|
+
|
|
18
35
|
<div className="flex items-start gap-3">
|
|
19
36
|
<span className="text-xl leading-none shrink-0 mt-0.5" suppressHydrationWarning>
|
|
20
37
|
{icon}
|