@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,11 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useEffect, useLayoutEffect, useRef, useState, useCallback } from 'react';
|
|
4
|
-
import { Sparkles, Send,
|
|
4
|
+
import { Sparkles, Send, StopCircle, SquarePen, History, X, Maximize2, Minimize2, PanelRight, AppWindow, Plus } from 'lucide-react';
|
|
5
5
|
import { useLocale } from '@/lib/LocaleContext';
|
|
6
|
-
import type { Message } from '@/lib/types';
|
|
6
|
+
import type { Message, ImagePart } from '@/lib/types';
|
|
7
7
|
import { useAskSession } from '@/hooks/useAskSession';
|
|
8
8
|
import { useFileUpload } from '@/hooks/useFileUpload';
|
|
9
|
+
import { useImageUpload } from '@/hooks/useImageUpload';
|
|
9
10
|
import { useMention } from '@/hooks/useMention';
|
|
10
11
|
import { useSlashCommand } from '@/hooks/useSlashCommand';
|
|
11
12
|
import type { SlashItem } from '@/hooks/useSlashCommand';
|
|
@@ -15,21 +16,18 @@ import SlashCommandPopover from '@/components/ask/SlashCommandPopover';
|
|
|
15
16
|
import SessionHistory from '@/components/ask/SessionHistory';
|
|
16
17
|
import SessionTabBar from '@/components/ask/SessionTabBar';
|
|
17
18
|
import FileChip from '@/components/ask/FileChip';
|
|
19
|
+
import AgentSelectorCapsule from '@/components/ask/AgentSelectorCapsule';
|
|
18
20
|
import { consumeUIMessageStream } from '@/lib/agent/stream-consumer';
|
|
19
21
|
import { isRetryableError, retryDelay, sleep } from '@/lib/agent/reconnect';
|
|
20
22
|
import { cn } from '@/lib/utils';
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
/** 输入框随内容增高,超过此行数后在框内滚动(与常见 IM 一致) */
|
|
30
|
-
const PANEL_TEXTAREA_MAX_VISIBLE_LINES = 8;
|
|
31
|
-
|
|
32
|
-
function syncTextareaToContent(el: HTMLTextAreaElement, maxVisibleLines: number, availableHeight?: number): void {
|
|
23
|
+
import { useAcpDetection } from '@/hooks/useAcpDetection';
|
|
24
|
+
import type { AcpAgentSelection } from '@/hooks/useAskModal';
|
|
25
|
+
|
|
26
|
+
/** Textarea auto-grows with content up to this many visible lines, then scrolls */
|
|
27
|
+
const TEXTAREA_MAX_VISIBLE_LINES = 8;
|
|
28
|
+
|
|
29
|
+
/** Auto-size textarea height to fit content, capped at maxVisibleLines */
|
|
30
|
+
function syncTextareaToContent(el: HTMLTextAreaElement, maxVisibleLines: number): void {
|
|
33
31
|
const style = getComputedStyle(el);
|
|
34
32
|
const parsedLh = parseFloat(style.lineHeight);
|
|
35
33
|
const parsedFs = parseFloat(style.fontSize);
|
|
@@ -38,33 +36,13 @@ function syncTextareaToContent(el: HTMLTextAreaElement, maxVisibleLines: number,
|
|
|
38
36
|
const pad =
|
|
39
37
|
(Number.isFinite(parseFloat(style.paddingTop)) ? parseFloat(style.paddingTop) : 0) +
|
|
40
38
|
(Number.isFinite(parseFloat(style.paddingBottom)) ? parseFloat(style.paddingBottom) : 0);
|
|
41
|
-
|
|
42
|
-
if (availableHeight && Number.isFinite(availableHeight) && availableHeight > 0) {
|
|
43
|
-
maxH = Math.min(maxH, availableHeight);
|
|
44
|
-
}
|
|
39
|
+
const maxH = lineHeight * maxVisibleLines + pad;
|
|
45
40
|
if (!Number.isFinite(maxH) || maxH <= 0) return;
|
|
46
41
|
el.style.height = '0px';
|
|
47
42
|
const next = Math.min(el.scrollHeight, maxH);
|
|
48
43
|
el.style.height = `${Number.isFinite(next) ? next : maxH}px`;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
function panelComposerMaxForViewport(): number {
|
|
52
|
-
if (typeof window === 'undefined') return PANEL_COMPOSER_MAX_ABS;
|
|
53
|
-
return Math.min(PANEL_COMPOSER_MAX_ABS, Math.floor(window.innerHeight * PANEL_COMPOSER_MAX_VIEW));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function readStoredPanelComposerHeight(): number {
|
|
57
|
-
if (typeof window === 'undefined') return PANEL_COMPOSER_DEFAULT;
|
|
58
|
-
try {
|
|
59
|
-
const s = localStorage.getItem(PANEL_COMPOSER_STORAGE);
|
|
60
|
-
if (s) {
|
|
61
|
-
const n = parseInt(s, 10);
|
|
62
|
-
if (Number.isFinite(n) && n >= PANEL_COMPOSER_MIN && n <= PANEL_COMPOSER_MAX_ABS) return n;
|
|
63
|
-
}
|
|
64
|
-
} catch {
|
|
65
|
-
/* ignore */
|
|
66
|
-
}
|
|
67
|
-
return PANEL_COMPOSER_DEFAULT;
|
|
44
|
+
// Only show scrollbar when content exceeds max height
|
|
45
|
+
el.style.overflowY = el.scrollHeight > maxH ? 'auto' : 'hidden';
|
|
68
46
|
}
|
|
69
47
|
|
|
70
48
|
interface AskContentProps {
|
|
@@ -72,6 +50,8 @@ interface AskContentProps {
|
|
|
72
50
|
visible: boolean;
|
|
73
51
|
currentFile?: string;
|
|
74
52
|
initialMessage?: string;
|
|
53
|
+
/** ACP agent pre-selected via "Use" button from A2A tab */
|
|
54
|
+
initialAcpAgent?: AcpAgentSelection | null;
|
|
75
55
|
onFirstMessage?: () => void;
|
|
76
56
|
/** 'modal' renders close button + ESC handler; 'panel' renders compact header */
|
|
77
57
|
variant: 'modal' | 'panel';
|
|
@@ -85,91 +65,16 @@ interface AskContentProps {
|
|
|
85
65
|
onModeSwitch?: () => void;
|
|
86
66
|
}
|
|
87
67
|
|
|
88
|
-
export default function AskContent({ visible, currentFile, initialMessage, onFirstMessage, variant, onClose, maximized, onMaximize, askMode, onModeSwitch }: AskContentProps) {
|
|
68
|
+
export default function AskContent({ visible, currentFile, initialMessage, initialAcpAgent, onFirstMessage, variant, onClose, maximized, onMaximize, askMode, onModeSwitch }: AskContentProps) {
|
|
89
69
|
const isPanel = variant === 'panel';
|
|
90
70
|
|
|
91
71
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
72
|
+
const imageInputRef = useRef<HTMLInputElement>(null);
|
|
92
73
|
const abortRef = useRef<AbortController | null>(null);
|
|
93
74
|
const firstMessageFired = useRef(false);
|
|
94
75
|
const { t } = useLocale();
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const panelComposerHRef = useRef(panelComposerHeight);
|
|
98
|
-
panelComposerHRef.current = panelComposerHeight;
|
|
99
|
-
|
|
100
|
-
useEffect(() => {
|
|
101
|
-
const stored = readStoredPanelComposerHeight();
|
|
102
|
-
if (stored !== PANEL_COMPOSER_DEFAULT) {
|
|
103
|
-
setPanelComposerHeight(stored);
|
|
104
|
-
panelComposerHRef.current = stored;
|
|
105
|
-
}
|
|
106
|
-
}, []);
|
|
107
|
-
|
|
108
|
-
const getPanelComposerHeight = useCallback(() => panelComposerHRef.current, []);
|
|
109
|
-
const persistPanelComposerHeight = useCallback((h: number) => {
|
|
110
|
-
try {
|
|
111
|
-
localStorage.setItem(PANEL_COMPOSER_STORAGE, String(h));
|
|
112
|
-
} catch {
|
|
113
|
-
/* ignore */
|
|
114
|
-
}
|
|
115
|
-
}, []);
|
|
116
|
-
|
|
117
|
-
const onPanelComposerResizePointerDown = useComposerVerticalResize({
|
|
118
|
-
minHeight: PANEL_COMPOSER_MIN,
|
|
119
|
-
maxHeightAbs: PANEL_COMPOSER_MAX_ABS,
|
|
120
|
-
maxHeightViewportRatio: PANEL_COMPOSER_MAX_VIEW,
|
|
121
|
-
getHeight: getPanelComposerHeight,
|
|
122
|
-
setHeight: setPanelComposerHeight,
|
|
123
|
-
persist: persistPanelComposerHeight,
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
const [panelComposerViewportMax, setPanelComposerViewportMax] = useState(PANEL_COMPOSER_MAX_ABS);
|
|
127
|
-
|
|
128
|
-
useEffect(() => {
|
|
129
|
-
setPanelComposerViewportMax(panelComposerMaxForViewport());
|
|
130
|
-
}, []);
|
|
131
|
-
|
|
132
|
-
const applyPanelComposerClampAndPersist = useCallback(() => {
|
|
133
|
-
const maxH = panelComposerMaxForViewport();
|
|
134
|
-
setPanelComposerViewportMax(maxH);
|
|
135
|
-
const h = panelComposerHRef.current;
|
|
136
|
-
if (h > maxH) {
|
|
137
|
-
setPanelComposerHeight(maxH);
|
|
138
|
-
panelComposerHRef.current = maxH;
|
|
139
|
-
persistPanelComposerHeight(maxH);
|
|
140
|
-
}
|
|
141
|
-
}, [persistPanelComposerHeight]);
|
|
142
|
-
|
|
143
|
-
const handlePanelComposerSeparatorKeyDown = useCallback(
|
|
144
|
-
(e: React.KeyboardEvent<HTMLElement>) => {
|
|
145
|
-
if (!['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(e.key)) return;
|
|
146
|
-
e.preventDefault();
|
|
147
|
-
const maxH = panelComposerMaxForViewport();
|
|
148
|
-
setPanelComposerViewportMax(maxH);
|
|
149
|
-
const h = panelComposerHRef.current;
|
|
150
|
-
let next = h;
|
|
151
|
-
if (e.key === 'ArrowUp') next = h + PANEL_COMPOSER_KEY_STEP;
|
|
152
|
-
else if (e.key === 'ArrowDown') next = h - PANEL_COMPOSER_KEY_STEP;
|
|
153
|
-
else if (e.key === 'Home') next = PANEL_COMPOSER_MIN;
|
|
154
|
-
else if (e.key === 'End') next = maxH;
|
|
155
|
-
const clamped = Math.round(Math.max(PANEL_COMPOSER_MIN, Math.min(maxH, next)));
|
|
156
|
-
setPanelComposerHeight(clamped);
|
|
157
|
-
panelComposerHRef.current = clamped;
|
|
158
|
-
persistPanelComposerHeight(clamped);
|
|
159
|
-
},
|
|
160
|
-
[persistPanelComposerHeight],
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
const resetPanelComposerHeight = useCallback(
|
|
164
|
-
(e: React.MouseEvent) => {
|
|
165
|
-
e.preventDefault();
|
|
166
|
-
e.stopPropagation();
|
|
167
|
-
setPanelComposerHeight(PANEL_COMPOSER_DEFAULT);
|
|
168
|
-
panelComposerHRef.current = PANEL_COMPOSER_DEFAULT;
|
|
169
|
-
persistPanelComposerHeight(PANEL_COMPOSER_DEFAULT);
|
|
170
|
-
},
|
|
171
|
-
[persistPanelComposerHeight],
|
|
172
|
-
);
|
|
76
|
+
const [mounted, setMounted] = useState(false);
|
|
77
|
+
useEffect(() => setMounted(true), []);
|
|
173
78
|
|
|
174
79
|
const [input, setInput] = useState('');
|
|
175
80
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -179,13 +84,17 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
179
84
|
const [attachedFiles, setAttachedFiles] = useState<string[]>([]);
|
|
180
85
|
const [showHistory, setShowHistory] = useState(false);
|
|
181
86
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
87
|
+
const [showAttachMenu, setShowAttachMenu] = useState(false);
|
|
182
88
|
|
|
183
89
|
const [selectedSkill, setSelectedSkill] = useState<SlashItem | null>(null);
|
|
90
|
+
const [selectedAcpAgent, setSelectedAcpAgent] = useState<AcpAgentSelection | null>(null);
|
|
184
91
|
|
|
185
92
|
const session = useAskSession(currentFile);
|
|
186
93
|
const upload = useFileUpload();
|
|
94
|
+
const imageUpload = useImageUpload();
|
|
187
95
|
const mention = useMention();
|
|
188
96
|
const slash = useSlashCommand();
|
|
97
|
+
const acpDetection = useAcpDetection();
|
|
189
98
|
|
|
190
99
|
useEffect(() => {
|
|
191
100
|
const handler = (e: Event) => {
|
|
@@ -215,11 +124,13 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
215
124
|
setInput(initialMessage || '');
|
|
216
125
|
firstMessageFired.current = false;
|
|
217
126
|
setAttachedFiles(currentFile ? [currentFile] : []);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
127
|
+
upload.clearAttachments();
|
|
128
|
+
imageUpload.clearImages();
|
|
129
|
+
mention.resetMention();
|
|
130
|
+
slash.resetSlash();
|
|
131
|
+
setSelectedSkill(null);
|
|
132
|
+
setSelectedAcpAgent(initialAcpAgent ?? null);
|
|
133
|
+
setShowHistory(false);
|
|
223
134
|
} else if (fileChanged) {
|
|
224
135
|
// Update attached file context to match new file (don't reset session/messages)
|
|
225
136
|
setAttachedFiles(currentFile ? [currentFile] : []);
|
|
@@ -259,12 +170,14 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
259
170
|
return () => window.removeEventListener('keydown', handler);
|
|
260
171
|
}, [variant, visible, onClose, mention, slash]);
|
|
261
172
|
|
|
173
|
+
// Close attach menu on any outside click
|
|
262
174
|
useEffect(() => {
|
|
263
|
-
if (!
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
175
|
+
if (!showAttachMenu) return;
|
|
176
|
+
const handler = () => setShowAttachMenu(false);
|
|
177
|
+
// Delay to avoid closing immediately from the click that opened it
|
|
178
|
+
const id = setTimeout(() => document.addEventListener('click', handler), 0);
|
|
179
|
+
return () => { clearTimeout(id); document.removeEventListener('click', handler); };
|
|
180
|
+
}, [showAttachMenu]);
|
|
268
181
|
|
|
269
182
|
const formRef = useRef<HTMLFormElement>(null);
|
|
270
183
|
|
|
@@ -272,11 +185,8 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
272
185
|
if (!visible) return;
|
|
273
186
|
const el = inputRef.current;
|
|
274
187
|
if (!el || !(el instanceof HTMLTextAreaElement)) return;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const availableH = isPanel && form ? form.clientHeight - 40 : undefined;
|
|
278
|
-
syncTextareaToContent(el, maxLines, availableH);
|
|
279
|
-
}, [input, isPanel, isLoading, visible, panelComposerHeight]);
|
|
188
|
+
syncTextareaToContent(el, TEXTAREA_MAX_VISIBLE_LINES);
|
|
189
|
+
}, [input, isLoading, visible]);
|
|
280
190
|
|
|
281
191
|
const mentionTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
282
192
|
const slashTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
@@ -372,12 +282,12 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
372
282
|
}
|
|
373
283
|
return;
|
|
374
284
|
}
|
|
375
|
-
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing && !isLoading && input.trim()) {
|
|
285
|
+
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing && !isLoading && (input.trim() || imageUpload.images.length > 0)) {
|
|
376
286
|
e.preventDefault();
|
|
377
287
|
(e.currentTarget as HTMLTextAreaElement).form?.requestSubmit();
|
|
378
288
|
}
|
|
379
289
|
},
|
|
380
|
-
[mention, selectMention, slash, selectSlashCommand, isLoading, input],
|
|
290
|
+
[mention, selectMention, slash, selectSlashCommand, isLoading, input, imageUpload.images],
|
|
381
291
|
);
|
|
382
292
|
|
|
383
293
|
const handleStop = useCallback(() => { abortRef.current?.abort(); }, []);
|
|
@@ -386,21 +296,22 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
386
296
|
e.preventDefault();
|
|
387
297
|
if (mention.mentionQuery !== null || slash.slashQuery !== null) return;
|
|
388
298
|
const text = input.trim();
|
|
389
|
-
if (!text || isLoading) return;
|
|
299
|
+
if ((!text && imageUpload.images.length === 0) || isLoading) return;
|
|
390
300
|
|
|
391
|
-
const
|
|
392
|
-
? `Use the skill ${selectedSkill.name}: ${text}`
|
|
393
|
-
: text;
|
|
301
|
+
const pendingImages = imageUpload.images.length > 0 ? [...imageUpload.images] : undefined;
|
|
394
302
|
const userMsg: Message = {
|
|
395
303
|
role: 'user',
|
|
396
|
-
content,
|
|
304
|
+
content: text, // No [ACP:] prefix — pass clean text
|
|
397
305
|
timestamp: Date.now(),
|
|
398
306
|
...(selectedSkill && { skillName: selectedSkill.name }),
|
|
307
|
+
...(pendingImages && { images: pendingImages }),
|
|
399
308
|
};
|
|
309
|
+
imageUpload.clearImages();
|
|
400
310
|
const requestMessages = [...session.messages, userMsg];
|
|
401
311
|
session.setMessages([...requestMessages, { role: 'assistant', content: '', timestamp: Date.now() }]);
|
|
402
312
|
setInput('');
|
|
403
313
|
setSelectedSkill(null);
|
|
314
|
+
setSelectedAcpAgent(null);
|
|
404
315
|
if (onFirstMessage && !firstMessageFired.current) {
|
|
405
316
|
firstMessageFired.current = true;
|
|
406
317
|
onFirstMessage();
|
|
@@ -430,6 +341,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
430
341
|
? f.content.slice(0, 20_000) + '\n\n[...truncated to first ~20000 chars]'
|
|
431
342
|
: f.content,
|
|
432
343
|
})),
|
|
344
|
+
selectedAcpAgent, // Send structured field instead of text prefix
|
|
433
345
|
});
|
|
434
346
|
|
|
435
347
|
const doFetch = async (): Promise<{ finalMessage: Message }> => {
|
|
@@ -547,7 +459,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
547
459
|
setReconnectAttempt(0);
|
|
548
460
|
abortRef.current = null;
|
|
549
461
|
}
|
|
550
|
-
}, [input, session, isLoading, currentFile, attachedFiles, upload.localAttachments, mention.mentionQuery, slash.slashQuery, selectedSkill, t.ask.errorNoResponse, t.ask.stopped, onFirstMessage]);
|
|
462
|
+
}, [input, session, isLoading, currentFile, attachedFiles, upload.localAttachments, imageUpload.images, imageUpload.clearImages, mention.mentionQuery, slash.slashQuery, selectedSkill, selectedAcpAgent, t.ask.errorNoResponse, t.ask.stopped, onFirstMessage]);
|
|
551
463
|
|
|
552
464
|
const handleResetSession = useCallback(() => {
|
|
553
465
|
if (isLoading) return;
|
|
@@ -555,15 +467,18 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
555
467
|
setInput('');
|
|
556
468
|
setAttachedFiles(currentFile ? [currentFile] : []);
|
|
557
469
|
upload.clearAttachments();
|
|
470
|
+
imageUpload.clearImages();
|
|
558
471
|
mention.resetMention();
|
|
559
472
|
slash.resetSlash();
|
|
560
473
|
setSelectedSkill(null);
|
|
474
|
+
setSelectedAcpAgent(null);
|
|
561
475
|
setShowHistory(false);
|
|
562
476
|
setTimeout(() => inputRef.current?.focus(), 0);
|
|
563
|
-
}, [isLoading, currentFile, session, upload, mention, slash]);
|
|
477
|
+
}, [isLoading, currentFile, session, upload, imageUpload, mention, slash]);
|
|
564
478
|
|
|
565
479
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
566
|
-
|
|
480
|
+
// Accept mindos file paths and image drops
|
|
481
|
+
if (e.dataTransfer.types.includes('text/mindos-path') || e.dataTransfer.types.includes('Files')) {
|
|
567
482
|
e.preventDefault();
|
|
568
483
|
e.dataTransfer.dropEffect = 'copy';
|
|
569
484
|
setIsDragOver(true);
|
|
@@ -572,14 +487,32 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
572
487
|
|
|
573
488
|
const handleDragLeave = useCallback(() => setIsDragOver(false), []);
|
|
574
489
|
|
|
575
|
-
const handleDrop = useCallback((e: React.DragEvent) => {
|
|
490
|
+
const handleDrop = useCallback(async (e: React.DragEvent) => {
|
|
576
491
|
e.preventDefault();
|
|
577
492
|
setIsDragOver(false);
|
|
493
|
+
// Try mindos file path first
|
|
578
494
|
const filePath = e.dataTransfer.getData('text/mindos-path');
|
|
579
495
|
if (filePath && !attachedFiles.includes(filePath)) {
|
|
580
496
|
setAttachedFiles(prev => [...prev, filePath]);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
// Try image drop
|
|
500
|
+
await imageUpload.handleDrop(e);
|
|
501
|
+
}, [attachedFiles, imageUpload]);
|
|
502
|
+
|
|
503
|
+
/** Handle paste — intercept images before normal text paste */
|
|
504
|
+
const handlePaste = useCallback((e: React.ClipboardEvent) => {
|
|
505
|
+
const items = e.clipboardData?.items;
|
|
506
|
+
if (!items) return;
|
|
507
|
+
// Check synchronously for image items — must preventDefault before awaiting
|
|
508
|
+
const hasImageItem = Array.from(items).some(
|
|
509
|
+
item => item.kind === 'file' && item.type.startsWith('image/')
|
|
510
|
+
);
|
|
511
|
+
if (hasImageItem) {
|
|
512
|
+
e.preventDefault();
|
|
513
|
+
void imageUpload.handlePaste(e);
|
|
581
514
|
}
|
|
582
|
-
}, [
|
|
515
|
+
}, [imageUpload]);
|
|
583
516
|
|
|
584
517
|
const handleLoadSession = useCallback((id: string) => {
|
|
585
518
|
session.loadSession(id);
|
|
@@ -587,14 +520,16 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
587
520
|
setInput('');
|
|
588
521
|
setAttachedFiles(currentFile ? [currentFile] : []);
|
|
589
522
|
upload.clearAttachments();
|
|
523
|
+
imageUpload.clearImages();
|
|
590
524
|
mention.resetMention();
|
|
591
525
|
slash.resetSlash();
|
|
592
526
|
setSelectedSkill(null);
|
|
527
|
+
setSelectedAcpAgent(null);
|
|
593
528
|
setTimeout(() => inputRef.current?.focus(), 0);
|
|
594
|
-
}, [session, currentFile, upload, mention, slash]);
|
|
529
|
+
}, [session, currentFile, upload, imageUpload, mention, slash]);
|
|
595
530
|
|
|
596
531
|
const iconSize = isPanel ? 13 : 14;
|
|
597
|
-
const inputIconSize =
|
|
532
|
+
const inputIconSize = 15;
|
|
598
533
|
|
|
599
534
|
return (
|
|
600
535
|
<>
|
|
@@ -679,7 +614,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
679
614
|
|
|
680
615
|
{/* Popovers — flex children so they stay within overflow boundary (absolute positioning would be clipped by RightAskPanel's overflow-hidden) */}
|
|
681
616
|
{mention.mentionQuery !== null && mention.mentionResults.length > 0 && (
|
|
682
|
-
<div className="shrink-0 px-
|
|
617
|
+
<div className="shrink-0 px-3 pb-1">
|
|
683
618
|
<MentionPopover
|
|
684
619
|
results={mention.mentionResults}
|
|
685
620
|
selectedIndex={mention.mentionIndex}
|
|
@@ -690,7 +625,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
690
625
|
)}
|
|
691
626
|
|
|
692
627
|
{slash.slashQuery !== null && slash.slashResults.length > 0 && (
|
|
693
|
-
<div className="shrink-0 px-
|
|
628
|
+
<div className="shrink-0 px-3 pb-1">
|
|
694
629
|
<SlashCommandPopover
|
|
695
630
|
results={slash.slashResults}
|
|
696
631
|
selectedIndex={slash.slashIndex}
|
|
@@ -700,99 +635,111 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
700
635
|
</div>
|
|
701
636
|
)}
|
|
702
637
|
|
|
703
|
-
{/* Input area —
|
|
638
|
+
{/* Input area — auto-height composer, no manual resize */}
|
|
704
639
|
<div
|
|
705
640
|
className={cn(
|
|
706
641
|
'shrink-0 border-t border-border',
|
|
707
|
-
isPanel && 'flex flex-col overflow-hidden bg-card',
|
|
708
642
|
isDragOver && 'ring-2 ring-[var(--amber)] ring-inset bg-[var(--amber-dim)]',
|
|
709
643
|
)}
|
|
710
|
-
style={isPanel ? { height: panelComposerHeight } : undefined}
|
|
711
644
|
onDragOver={handleDragOver}
|
|
712
645
|
onDragLeave={handleDragLeave}
|
|
713
646
|
onDrop={handleDrop}
|
|
714
647
|
>
|
|
715
|
-
{isPanel ? (
|
|
716
|
-
<div
|
|
717
|
-
role="separator"
|
|
718
|
-
tabIndex={0}
|
|
719
|
-
aria-orientation="horizontal"
|
|
720
|
-
aria-label={`${t.ask.panelComposerResize}. ${t.ask.panelComposerResetHint}. ${t.ask.panelComposerKeyboard}`}
|
|
721
|
-
aria-valuemin={PANEL_COMPOSER_MIN}
|
|
722
|
-
aria-valuemax={panelComposerViewportMax}
|
|
723
|
-
aria-valuenow={panelComposerHeight}
|
|
724
|
-
title={`${t.ask.panelComposerResize} · ${t.ask.panelComposerResetHint} · ${t.ask.panelComposerKeyboard}`}
|
|
725
|
-
onPointerDown={onPanelComposerResizePointerDown}
|
|
726
|
-
onKeyDown={handlePanelComposerSeparatorKeyDown}
|
|
727
|
-
onDoubleClick={resetPanelComposerHeight}
|
|
728
|
-
className="group flex h-3 shrink-0 cursor-ns-resize items-center justify-center border-b border-border/50 bg-muted/[0.06] transition-colors hover:bg-muted/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
|
|
729
|
-
>
|
|
730
|
-
<span
|
|
731
|
-
className="pointer-events-none h-1 w-10 rounded-full bg-border transition-colors group-hover:bg-[var(--amber)]/45 group-active:bg-[var(--amber)]/60"
|
|
732
|
-
aria-hidden
|
|
733
|
-
/>
|
|
734
|
-
</div>
|
|
735
|
-
) : null}
|
|
736
648
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
649
|
+
{/* Unified context chip flow */}
|
|
650
|
+
{(attachedFiles.length > 0 || upload.localAttachments.length > 0 || imageUpload.images.length > 0 || selectedSkill || selectedAcpAgent || upload.uploadError || imageUpload.imageError) && (
|
|
651
|
+
<div className={cn('shrink-0 px-3 pt-2 pb-1', isPanel ? 'max-h-24 overflow-y-auto' : 'max-h-28 overflow-y-auto')}>
|
|
652
|
+
<div className="flex flex-wrap gap-1.5">
|
|
653
|
+
{/* KB files (@ attached) */}
|
|
654
|
+
{attachedFiles.map(f => (
|
|
655
|
+
<FileChip key={f} path={f} variant="kb" onRemove={() => setAttachedFiles(prev => prev.filter(x => x !== f))} />
|
|
656
|
+
))}
|
|
657
|
+
{/* Uploaded files */}
|
|
658
|
+
{upload.localAttachments.map((f, idx) => (
|
|
659
|
+
<FileChip key={`up-${f.name}-${idx}`} path={f.name} variant="upload" onRemove={() => upload.removeAttachment(idx)} />
|
|
660
|
+
))}
|
|
661
|
+
{/* Images (name chip + hover preview) */}
|
|
662
|
+
{imageUpload.images.map((img, idx) => (
|
|
663
|
+
<FileChip
|
|
664
|
+
key={`img-${idx}`}
|
|
665
|
+
path={`Image ${idx + 1}`}
|
|
666
|
+
variant="image"
|
|
667
|
+
imageData={img.data}
|
|
668
|
+
imageMime={img.mimeType}
|
|
669
|
+
onRemove={() => imageUpload.removeImage(idx)}
|
|
670
|
+
/>
|
|
671
|
+
))}
|
|
672
|
+
{/* Skill */}
|
|
673
|
+
{selectedSkill && (
|
|
674
|
+
<FileChip
|
|
675
|
+
path={selectedSkill.name}
|
|
676
|
+
variant="skill"
|
|
677
|
+
onRemove={() => { setSelectedSkill(null); inputRef.current?.focus(); }}
|
|
678
|
+
/>
|
|
679
|
+
)}
|
|
680
|
+
{/* Agent */}
|
|
681
|
+
{selectedAcpAgent && (
|
|
682
|
+
<FileChip
|
|
683
|
+
path={selectedAcpAgent.name}
|
|
684
|
+
variant="agent"
|
|
685
|
+
onRemove={() => { setSelectedAcpAgent(null); inputRef.current?.focus(); }}
|
|
686
|
+
/>
|
|
687
|
+
)}
|
|
748
688
|
</div>
|
|
749
|
-
|
|
689
|
+
{/* Errors (merged) */}
|
|
690
|
+
{(upload.uploadError || imageUpload.imageError) && (
|
|
691
|
+
<div className="mt-1 text-xs text-error">{upload.uploadError || imageUpload.imageError}</div>
|
|
692
|
+
)}
|
|
693
|
+
</div>
|
|
694
|
+
)}
|
|
750
695
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
)}
|
|
696
|
+
{/* Agent selector — only when no agent selected but agents available (mounted guard for hydration) */}
|
|
697
|
+
{mounted && !selectedAcpAgent && acpDetection.installedAgents.length > 0 && (
|
|
698
|
+
<div className="px-3 pt-1 pb-0.5">
|
|
699
|
+
<AgentSelectorCapsule
|
|
700
|
+
selectedAgent={null}
|
|
701
|
+
onSelect={setSelectedAcpAgent}
|
|
702
|
+
installedAgents={acpDetection.installedAgents}
|
|
703
|
+
loading={acpDetection.loading}
|
|
704
|
+
/>
|
|
705
|
+
</div>
|
|
706
|
+
)}
|
|
763
707
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
708
|
+
{/* Input form */}
|
|
709
|
+
<form
|
|
710
|
+
ref={formRef}
|
|
711
|
+
onSubmit={handleSubmit}
|
|
712
|
+
className="flex items-end gap-1.5 px-3 py-2"
|
|
713
|
+
>
|
|
714
|
+
{/* + attach button with mini menu */}
|
|
715
|
+
<div className="relative shrink-0">
|
|
716
|
+
<button
|
|
717
|
+
type="button"
|
|
718
|
+
onClick={() => setShowAttachMenu(v => !v)}
|
|
719
|
+
className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
720
|
+
title={t.hints.attachFile}
|
|
721
|
+
>
|
|
722
|
+
<Plus size={inputIconSize} />
|
|
723
|
+
</button>
|
|
724
|
+
{showAttachMenu && (
|
|
725
|
+
<div className="absolute bottom-full left-0 mb-1 py-1 rounded-lg border border-border bg-card shadow-lg z-50 min-w-[140px]">
|
|
769
726
|
<button
|
|
770
727
|
type="button"
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
aria-label={`Remove skill ${selectedSkill.name}`}
|
|
728
|
+
className="flex w-full items-center gap-2 px-3 py-1.5 text-xs hover:bg-muted transition-colors text-left"
|
|
729
|
+
onClick={() => { setShowAttachMenu(false); upload.uploadInputRef.current?.click(); }}
|
|
774
730
|
>
|
|
775
|
-
|
|
731
|
+
File
|
|
776
732
|
</button>
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
<form
|
|
786
|
-
ref={formRef}
|
|
787
|
-
onSubmit={handleSubmit}
|
|
788
|
-
className={cn(
|
|
789
|
-
'flex',
|
|
790
|
-
isPanel ? 'min-h-0 flex-1 items-end gap-1.5 px-2 py-2' : 'items-end gap-2 px-3 py-3',
|
|
733
|
+
<button
|
|
734
|
+
type="button"
|
|
735
|
+
className="flex w-full items-center gap-2 px-3 py-1.5 text-xs hover:bg-muted transition-colors text-left"
|
|
736
|
+
onClick={() => { setShowAttachMenu(false); imageInputRef.current?.click(); }}
|
|
737
|
+
>
|
|
738
|
+
Image
|
|
739
|
+
</button>
|
|
740
|
+
</div>
|
|
791
741
|
)}
|
|
792
|
-
>
|
|
793
|
-
<button type="button" onClick={() => upload.uploadInputRef.current?.click()} className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0" title={t.hints.attachFile}>
|
|
794
|
-
<Paperclip size={inputIconSize} />
|
|
795
|
-
</button>
|
|
742
|
+
</div>
|
|
796
743
|
|
|
797
744
|
<input
|
|
798
745
|
ref={upload.uploadInputRef}
|
|
@@ -806,6 +753,18 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
806
753
|
inputEl.value = '';
|
|
807
754
|
}}
|
|
808
755
|
/>
|
|
756
|
+
<input
|
|
757
|
+
ref={imageInputRef}
|
|
758
|
+
type="file"
|
|
759
|
+
className="hidden"
|
|
760
|
+
multiple
|
|
761
|
+
accept="image/png,image/jpeg,image/gif,image/webp"
|
|
762
|
+
onChange={async (e) => {
|
|
763
|
+
const inputEl = e.currentTarget;
|
|
764
|
+
await imageUpload.handleFileSelect(inputEl.files);
|
|
765
|
+
inputEl.value = '';
|
|
766
|
+
}}
|
|
767
|
+
/>
|
|
809
768
|
|
|
810
769
|
<textarea
|
|
811
770
|
ref={(el) => {
|
|
@@ -814,12 +773,10 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
814
773
|
value={input}
|
|
815
774
|
onChange={e => handleInputChange(e.target.value, e.target.selectionStart ?? undefined)}
|
|
816
775
|
onKeyDown={handleInputKeyDown}
|
|
776
|
+
onPaste={handlePaste}
|
|
817
777
|
placeholder={t.ask.placeholder}
|
|
818
778
|
rows={1}
|
|
819
|
-
className=
|
|
820
|
-
'min-w-0 flex-1 resize-none overflow-y-auto bg-transparent text-sm leading-snug text-foreground placeholder:text-muted-foreground outline-none focus-visible:ring-0',
|
|
821
|
-
isPanel ? 'py-2' : 'py-1.5',
|
|
822
|
-
)}
|
|
779
|
+
className="min-w-0 flex-1 resize-none overflow-y-hidden bg-transparent py-1.5 text-sm leading-snug text-foreground placeholder:text-muted-foreground outline-none focus-visible:ring-0"
|
|
823
780
|
/>
|
|
824
781
|
|
|
825
782
|
{isLoading ? (
|
|
@@ -827,30 +784,31 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
827
784
|
{loadingPhase === 'reconnecting' ? <X size={inputIconSize} /> : <StopCircle size={inputIconSize} />}
|
|
828
785
|
</button>
|
|
829
786
|
) : (
|
|
830
|
-
<button type="submit" disabled={!input.trim()
|
|
831
|
-
<Send size={
|
|
787
|
+
<button type="submit" disabled={!input.trim() && imageUpload.images.length === 0} className="p-1.5 rounded-md disabled:opacity-40 disabled:cursor-not-allowed transition-opacity shrink-0 bg-[var(--amber)] text-[var(--amber-foreground)]">
|
|
788
|
+
<Send size={14} />
|
|
832
789
|
</button>
|
|
833
790
|
)}
|
|
834
|
-
|
|
835
|
-
</div>
|
|
791
|
+
</form>
|
|
836
792
|
</div>
|
|
837
793
|
|
|
838
|
-
{/* Footer hints —
|
|
794
|
+
{/* Footer hints — panel: compact 3 items; modal: full set */}
|
|
839
795
|
<div
|
|
840
796
|
className={cn(
|
|
841
|
-
'flex shrink-0 items-center',
|
|
797
|
+
'flex shrink-0 items-center flex-wrap px-3 pb-1.5',
|
|
842
798
|
isPanel
|
|
843
|
-
? '
|
|
844
|
-
: '
|
|
799
|
+
? 'gap-x-3 gap-y-1 text-[10px] text-muted-foreground/40'
|
|
800
|
+
: 'gap-x-3 gap-y-1 text-[10px] md:text-xs text-muted-foreground/50',
|
|
845
801
|
)}
|
|
846
802
|
>
|
|
847
803
|
<span suppressHydrationWarning>
|
|
848
804
|
<kbd className="font-mono">↵</kbd> {t.ask.send}
|
|
849
805
|
</span>
|
|
850
|
-
|
|
851
|
-
<
|
|
852
|
-
|
|
853
|
-
|
|
806
|
+
{!isPanel && (
|
|
807
|
+
<span suppressHydrationWarning>
|
|
808
|
+
<kbd className="font-mono">⇧</kbd>
|
|
809
|
+
<kbd className="font-mono ml-0.5">↵</kbd> {t.ask.newlineHint}
|
|
810
|
+
</span>
|
|
811
|
+
)}
|
|
854
812
|
<span suppressHydrationWarning>
|
|
855
813
|
<kbd className="font-mono">@</kbd> {t.ask.attachFile}
|
|
856
814
|
</span>
|
|
@@ -863,7 +821,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
863
821
|
</span>
|
|
864
822
|
)}
|
|
865
823
|
{isLoading && input.trim() && (
|
|
866
|
-
<span className=
|
|
824
|
+
<span className="text-[10px] text-[var(--amber)]/80">
|
|
867
825
|
{t.ask.draftingHint}
|
|
868
826
|
</span>
|
|
869
827
|
)}
|