claude-ws 0.3.97 → 0.3.99
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/locales/de.json +374 -12
- package/locales/en.json +374 -12
- package/locales/es.json +398 -11
- package/locales/fr.json +398 -11
- package/locales/ja.json +398 -11
- package/locales/ko.json +398 -11
- package/locales/vi.json +374 -12
- package/locales/zh.json +398 -11
- package/package.json +1 -1
- package/server.ts +283 -6
- package/src/app/[locale]/not-found.tsx +6 -3
- package/src/app/[locale]/page.tsx +14 -4
- package/src/app/api/attempts/[id]/workflow/route.ts +76 -0
- package/src/app/api/questions/answer/route.ts +58 -0
- package/src/app/api/questions/route.ts +68 -0
- package/src/app/api/tasks/[id]/compact/route.ts +62 -0
- package/src/components/access-anywhere/api-access-key-setup-modal.tsx +2 -2
- package/src/components/access-anywhere/tunnel-settings-dialog.tsx +6 -6
- package/src/components/access-anywhere/wizard-step-ctunnel.tsx +8 -8
- package/src/components/agent-factory/dependency-tree.tsx +5 -3
- package/src/components/agent-factory/discovery-dialog.tsx +26 -22
- package/src/components/agent-factory/plugin-detail-dialog.tsx +41 -38
- package/src/components/agent-factory/plugin-form-dialog.tsx +23 -20
- package/src/components/agent-factory/plugin-list.tsx +20 -17
- package/src/components/agent-factory/upload-dialog.tsx +17 -14
- package/src/components/auth/agent-provider-dialog.tsx +67 -65
- package/src/components/auth/api-key-dialog.tsx +14 -11
- package/src/components/auth/auth-error-message.tsx +6 -3
- package/src/components/editor/code-editor-with-inline-edit.tsx +4 -2
- package/src/components/editor/file-diff-resolver-modal.tsx +31 -26
- package/src/components/editor/inline-edit-dialog.tsx +9 -6
- package/src/components/editor/selection-mention-popup.tsx +3 -1
- package/src/components/header/project-selector.tsx +7 -4
- package/src/components/header.tsx +70 -4
- package/src/components/kanban/column.tsx +11 -0
- package/src/components/kanban/task-card.tsx +70 -4
- package/src/components/project-settings/component-selector.tsx +3 -1
- package/src/components/project-settings/plugin-upload-dialog.tsx +7 -5
- package/src/components/project-settings/project-settings-dialog.tsx +5 -3
- package/src/components/questions/questions-panel.tsx +136 -0
- package/src/components/settings/folder-browser-dialog.tsx +29 -25
- package/src/components/settings/settings-page.tsx +64 -18
- package/src/components/settings/setup-dialog.tsx +26 -23
- package/src/components/setup/unified-setup-wizard.tsx +12 -9
- package/src/components/sidebar/file-browser/file-create-buttons.tsx +7 -3
- package/src/components/sidebar/file-browser/file-tab-content.tsx +19 -15
- package/src/components/sidebar/file-browser/file-tabs-panel.tsx +7 -4
- package/src/components/sidebar/file-browser/file-tree.tsx +3 -1
- package/src/components/sidebar/git-changes/branch-checkout-modal.tsx +6 -4
- package/src/components/sidebar/git-changes/commit-details-modal.tsx +5 -3
- package/src/components/sidebar/git-changes/diff-tabs-panel.tsx +3 -1
- package/src/components/sidebar/git-changes/git-file-item.tsx +8 -6
- package/src/components/sidebar/git-changes/git-graph.tsx +8 -5
- package/src/components/sidebar/git-changes/git-panel.tsx +28 -27
- package/src/components/sidebar/git-changes/git-section.tsx +5 -3
- package/src/components/sidebar/shells/shell-panel.tsx +3 -1
- package/src/components/task/attachment-bar.tsx +4 -1
- package/src/components/task/attempt-item.tsx +7 -5
- package/src/components/task/conversation-view.tsx +21 -13
- package/src/components/task/floating-chat-window.tsx +14 -5
- package/src/components/task/interactive-command/checkpoint-list.tsx +5 -3
- package/src/components/task/interactive-command/confirm-dialog.tsx +9 -4
- package/src/components/task/interactive-command/interactive-command-overlay.tsx +23 -9
- package/src/components/task/interactive-command/question-prompt.tsx +12 -8
- package/src/components/task/pending-question-indicator.tsx +5 -3
- package/src/components/task/prompt-input.tsx +1 -1
- package/src/components/task/shell-log-view.tsx +3 -1
- package/src/components/task/status-line.tsx +84 -23
- package/src/components/task/task-detail-panel.tsx +27 -27
- package/src/components/task/task-shell-indicator.tsx +10 -6
- package/src/components/terminal/terminal-context-menu.tsx +6 -4
- package/src/components/terminal/terminal-instance.tsx +11 -3
- package/src/components/terminal/terminal-panel.tsx +6 -3
- package/src/components/terminal/terminal-shortcut-bar.tsx +3 -1
- package/src/components/terminal/terminal-tab-bar.tsx +5 -3
- package/src/components/workflow/workflow-panel.tsx +181 -0
- package/src/hooks/use-attempt-stream.ts +96 -3
- package/src/lib/agent-manager.ts +89 -3
- package/src/lib/db/index.ts +18 -0
- package/src/lib/db/schema.ts +29 -0
- package/src/lib/process-manager.ts +28 -7
- package/src/lib/session-manager.ts +60 -0
- package/src/lib/usage-tracker.ts +19 -19
- package/src/lib/workflow-tracker.ts +118 -20
- package/src/stores/questions-store.ts +76 -0
- package/src/stores/workflow-store.ts +71 -0
|
@@ -1,14 +1,45 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
3
4
|
import { useSortable } from '@dnd-kit/sortable';
|
|
4
5
|
import { CSS } from '@dnd-kit/utilities';
|
|
5
6
|
import { Task } from '@/types';
|
|
6
7
|
import { cn, getProjectColor } from '@/lib/utils';
|
|
7
|
-
import { GripVertical, MessageSquare, Trash2, Search } from 'lucide-react';
|
|
8
|
+
import { GripVertical, MessageSquare, Trash2, Search, Network } from 'lucide-react';
|
|
9
|
+
import { useTranslations } from 'next-intl';
|
|
8
10
|
import { useTaskStore } from '@/stores/task-store';
|
|
9
11
|
import { useProjectStore } from '@/stores/project-store';
|
|
12
|
+
import { useQuestionsStore } from '@/stores/questions-store';
|
|
13
|
+
import { useWorkflowStore } from '@/stores/workflow-store';
|
|
10
14
|
import type { ChatHistoryMatch } from '@/hooks/use-chat-history-search';
|
|
11
15
|
|
|
16
|
+
function formatRelativeTime(timestamp: number): string {
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
const diffMs = now - timestamp;
|
|
19
|
+
const diffMin = Math.floor(diffMs / 60000);
|
|
20
|
+
const diffHr = Math.floor(diffMs / 3600000);
|
|
21
|
+
const diffDay = Math.floor(diffMs / 86400000);
|
|
22
|
+
|
|
23
|
+
if (diffMin < 1) return 'just now';
|
|
24
|
+
if (diffMin < 60) return `${diffMin}m ago`;
|
|
25
|
+
if (diffHr < 24) return `${diffHr}h ago`;
|
|
26
|
+
if (diffDay < 30) return `${diffDay}d ago`;
|
|
27
|
+
return new Date(timestamp).toLocaleDateString();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function formatAbsoluteTime(timestamp: number): string {
|
|
31
|
+
return new Date(timestamp).toLocaleString();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function RelativeTime({ timestamp }: { timestamp: number }) {
|
|
35
|
+
const [, setTick] = useState(0);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const interval = setInterval(() => setTick((t) => t + 1), 60000);
|
|
38
|
+
return () => clearInterval(interval);
|
|
39
|
+
}, []);
|
|
40
|
+
return <>{formatRelativeTime(timestamp)}</>;
|
|
41
|
+
}
|
|
42
|
+
|
|
12
43
|
interface TaskCardProps {
|
|
13
44
|
task: Task;
|
|
14
45
|
attemptCount?: number;
|
|
@@ -20,7 +51,14 @@ interface TaskCardProps {
|
|
|
20
51
|
export function TaskCard({ task, attemptCount = 0, searchQuery = '', isMobile = false, chatHistoryMatch }: TaskCardProps) {
|
|
21
52
|
const { selectedTaskId, selectTask, deleteTask } = useTaskStore();
|
|
22
53
|
const { projects, selectedProjectIds, isAllProjectsMode } = useProjectStore();
|
|
54
|
+
const { getByTaskId } = useQuestionsStore();
|
|
55
|
+
const { getByTaskId: getWorkflowByTaskId } = useWorkflowStore();
|
|
56
|
+
const tTask = useTranslations('task');
|
|
57
|
+
const tKanban = useTranslations('kanban');
|
|
23
58
|
const isSelected = selectedTaskId === task.id;
|
|
59
|
+
const hasPendingQuestion = !!getByTaskId(task.id);
|
|
60
|
+
const workflowEntry = getWorkflowByTaskId(task.id);
|
|
61
|
+
const hasActiveWorkflow = workflowEntry && workflowEntry.summary.activeCount > 0;
|
|
24
62
|
|
|
25
63
|
// Helper function to highlight matched text
|
|
26
64
|
const highlightText = (text: string) => {
|
|
@@ -47,7 +85,7 @@ export function TaskCard({ task, attemptCount = 0, searchQuery = '', isMobile =
|
|
|
47
85
|
|
|
48
86
|
const handleDelete = async (e: React.MouseEvent) => {
|
|
49
87
|
e.stopPropagation();
|
|
50
|
-
if (!confirm(
|
|
88
|
+
if (!confirm(tTask('deleteTaskConfirm', { title: task.title }))) return;
|
|
51
89
|
try {
|
|
52
90
|
await fetch(`/api/tasks/${task.id}`, { method: 'DELETE' });
|
|
53
91
|
deleteTask(task.id);
|
|
@@ -115,11 +153,31 @@ export function TaskCard({ task, attemptCount = 0, searchQuery = '', isMobile =
|
|
|
115
153
|
: '-left-1 -translate-x-full opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none',
|
|
116
154
|
'hover:bg-muted'
|
|
117
155
|
)}
|
|
118
|
-
aria-label=
|
|
156
|
+
aria-label={tKanban('dragToReorder')}
|
|
119
157
|
>
|
|
120
158
|
<GripVertical className="size-4" />
|
|
121
159
|
</button>
|
|
122
160
|
|
|
161
|
+
{/* Pending question indicator dot */}
|
|
162
|
+
{hasPendingQuestion && (
|
|
163
|
+
<span
|
|
164
|
+
className="absolute top-1.5 right-1.5 size-2 rounded-full bg-amber-500 z-10"
|
|
165
|
+
title="Pending question"
|
|
166
|
+
/>
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
{/* Active workflow indicator */}
|
|
170
|
+
{hasActiveWorkflow && (
|
|
171
|
+
<span
|
|
172
|
+
className="absolute top-1.5 flex items-center gap-0.5 text-[9px] font-medium text-blue-500 z-10"
|
|
173
|
+
style={{ right: hasPendingQuestion ? '1rem' : '0.375rem' }}
|
|
174
|
+
title={`${workflowEntry.summary.activeCount} agent${workflowEntry.summary.activeCount !== 1 ? 's' : ''} running`}
|
|
175
|
+
>
|
|
176
|
+
<Network className="size-2.5" />
|
|
177
|
+
<span>{workflowEntry.summary.activeCount}</span>
|
|
178
|
+
</span>
|
|
179
|
+
)}
|
|
180
|
+
|
|
123
181
|
{/* Delete button - always visible for Done/Cancelled tasks */}
|
|
124
182
|
{showDeleteButton && (
|
|
125
183
|
<button
|
|
@@ -129,7 +187,7 @@ export function TaskCard({ task, attemptCount = 0, searchQuery = '', isMobile =
|
|
|
129
187
|
'text-muted-foreground hover:text-destructive',
|
|
130
188
|
'hover:bg-muted pointer-events-auto z-10'
|
|
131
189
|
)}
|
|
132
|
-
aria-label=
|
|
190
|
+
aria-label={tKanban('deleteTask')}
|
|
133
191
|
>
|
|
134
192
|
<Trash2 className="size-3" />
|
|
135
193
|
</button>
|
|
@@ -186,6 +244,14 @@ export function TaskCard({ task, attemptCount = 0, searchQuery = '', isMobile =
|
|
|
186
244
|
</div>
|
|
187
245
|
</div>
|
|
188
246
|
)}
|
|
247
|
+
|
|
248
|
+
{/* Timestamp - shows relative time, switches to exact time on card hover */}
|
|
249
|
+
{task.updatedAt && (
|
|
250
|
+
<div className="mt-1.5 text-[10px] text-muted-foreground/70">
|
|
251
|
+
<span className="group-hover:hidden"><RelativeTime timestamp={task.updatedAt} /></span>
|
|
252
|
+
<span className="hidden group-hover:inline">{formatAbsoluteTime(task.updatedAt)}</span>
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
189
255
|
</div>
|
|
190
256
|
</div>
|
|
191
257
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
|
+
import { useTranslations } from 'next-intl';
|
|
4
5
|
import { Search, Check, Loader2, Trash2, Upload } from 'lucide-react';
|
|
5
6
|
import { Input } from '@/components/ui/input';
|
|
6
7
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
@@ -24,6 +25,7 @@ interface InstalledStatus {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export function ComponentSelector({ type, selectedIds, onChange, projectId, installedIds = [], onRefresh, onCloseDialog }: ComponentSelectorProps) {
|
|
28
|
+
const t = useTranslations('agentFactory');
|
|
27
29
|
const [components, setComponents] = useState<AgentFactoryPlugin[]>([]);
|
|
28
30
|
const [loading, setLoading] = useState(true);
|
|
29
31
|
const [searchQuery, setSearchQuery] = useState('');
|
|
@@ -113,7 +115,7 @@ export function ComponentSelector({ type, selectedIds, onChange, projectId, inst
|
|
|
113
115
|
onRefresh?.();
|
|
114
116
|
} catch (error) {
|
|
115
117
|
console.error('Error uninstalling component:', error);
|
|
116
|
-
alert('
|
|
118
|
+
alert(t('failedToUninstallComponent'));
|
|
117
119
|
} finally {
|
|
118
120
|
setUninstalling(null);
|
|
119
121
|
}
|
|
@@ -13,6 +13,7 @@ import { Badge } from '@/components/ui/badge';
|
|
|
13
13
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
14
14
|
import { Label } from '@/components/ui/label';
|
|
15
15
|
import { Upload, FileArchive, Loader2, AlertCircle, Package, FileText, Folder, Check, X } from 'lucide-react';
|
|
16
|
+
import { useTranslations } from 'next-intl';
|
|
16
17
|
|
|
17
18
|
interface PreviewItem {
|
|
18
19
|
type: 'skill' | 'command' | 'agent' | 'agent_set' | 'unknown';
|
|
@@ -35,6 +36,7 @@ interface PluginUploadDialogProps {
|
|
|
35
36
|
* - Import to agent factory as well
|
|
36
37
|
*/
|
|
37
38
|
export function PluginUploadDialog({ open, onOpenChange, projectId, onUploadSuccess }: PluginUploadDialogProps) {
|
|
39
|
+
const t = useTranslations('agentFactory');
|
|
38
40
|
const [step, setStep] = useState<'upload' | 'preview' | 'importing'>('upload');
|
|
39
41
|
const [uploading, setUploading] = useState(false);
|
|
40
42
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -370,15 +372,15 @@ export function PluginUploadDialog({ open, onOpenChange, projectId, onUploadSucc
|
|
|
370
372
|
{uploading ? (
|
|
371
373
|
<div className="flex flex-col items-center gap-3">
|
|
372
374
|
<Loader2 className="w-12 h-12 animate-spin text-muted-foreground" />
|
|
373
|
-
<p className="text-muted-foreground">
|
|
375
|
+
<p className="text-muted-foreground">{t('analyzingArchive')}</p>
|
|
374
376
|
</div>
|
|
375
377
|
) : (
|
|
376
378
|
<div className="flex flex-col items-center gap-3">
|
|
377
379
|
<Upload className="w-12 h-12 text-muted-foreground" />
|
|
378
380
|
<div>
|
|
379
|
-
<p className="font-medium">
|
|
381
|
+
<p className="font-medium">{t('clickToUploadOrDrag')}</p>
|
|
380
382
|
<p className="text-sm text-muted-foreground mt-1">
|
|
381
|
-
|
|
383
|
+
{t('supportedFormats')}
|
|
382
384
|
</p>
|
|
383
385
|
</div>
|
|
384
386
|
</div>
|
|
@@ -395,7 +397,7 @@ export function PluginUploadDialog({ open, onOpenChange, projectId, onUploadSucc
|
|
|
395
397
|
|
|
396
398
|
{/* Info */}
|
|
397
399
|
<div className="text-sm text-muted-foreground bg-muted/50 p-3 rounded-lg">
|
|
398
|
-
<p className="font-medium mb-2">
|
|
400
|
+
<p className="font-medium mb-2">{t('automaticOrganization')}</p>
|
|
399
401
|
<p className="text-xs mb-2">
|
|
400
402
|
Files will be automatically organized into the correct folders:
|
|
401
403
|
</p>
|
|
@@ -495,7 +497,7 @@ export function PluginUploadDialog({ open, onOpenChange, projectId, onUploadSucc
|
|
|
495
497
|
{step === 'importing' && (
|
|
496
498
|
<div className="flex flex-col items-center gap-4 py-8">
|
|
497
499
|
<Loader2 className="w-12 h-12 animate-spin text-muted-foreground" />
|
|
498
|
-
<p className="text-muted-foreground">
|
|
500
|
+
<p className="text-muted-foreground">{t('importingPlugins')}</p>
|
|
499
501
|
</div>
|
|
500
502
|
)}
|
|
501
503
|
</div>
|
|
@@ -19,6 +19,7 @@ import { ComponentSelector } from './component-selector';
|
|
|
19
19
|
import { useProjectSettingsStore } from '@/stores/project-settings-store';
|
|
20
20
|
import { useAgentFactoryUIStore } from '@/stores/agent-factory-ui-store';
|
|
21
21
|
import { useToast } from '@/hooks/use-toast';
|
|
22
|
+
import { useTranslations } from 'next-intl';
|
|
22
23
|
|
|
23
24
|
interface InstallResult {
|
|
24
25
|
installed: string[];
|
|
@@ -33,6 +34,7 @@ interface ProjectSettingsDialogProps {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export function ProjectSettingsDialog({ open, onOpenChange, projectId }: ProjectSettingsDialogProps) {
|
|
37
|
+
const t = useTranslations('settings');
|
|
36
38
|
const { projects } = useProjectStore();
|
|
37
39
|
const { setOpen: setAgentFactoryOpen } = useAgentFactoryUIStore();
|
|
38
40
|
const {
|
|
@@ -155,8 +157,8 @@ export function ProjectSettingsDialog({ open, onOpenChange, projectId }: Project
|
|
|
155
157
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
156
158
|
<DialogContent>
|
|
157
159
|
<DialogHeader>
|
|
158
|
-
<DialogTitle>
|
|
159
|
-
<DialogDescription>
|
|
160
|
+
<DialogTitle>{t('projectSettings')}</DialogTitle>
|
|
161
|
+
<DialogDescription>{t('noProjectsAvailable')}</DialogDescription>
|
|
160
162
|
</DialogHeader>
|
|
161
163
|
</DialogContent>
|
|
162
164
|
</Dialog>
|
|
@@ -169,7 +171,7 @@ export function ProjectSettingsDialog({ open, onOpenChange, projectId }: Project
|
|
|
169
171
|
<DialogHeader>
|
|
170
172
|
<DialogTitle className="flex items-center gap-2">
|
|
171
173
|
<Settings className="h-5 w-5" />
|
|
172
|
-
{selectedProject?.name || '
|
|
174
|
+
{selectedProject?.name || t('projectSettings')}
|
|
173
175
|
</DialogTitle>
|
|
174
176
|
<DialogDescription>
|
|
175
177
|
Configure plugins and agent sets for this project
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { MessageCircleQuestion, X } from 'lucide-react';
|
|
4
|
+
import { Badge } from '@/components/ui/badge';
|
|
5
|
+
import { Button } from '@/components/ui/button';
|
|
6
|
+
import { useQuestionsStore, type PendingQuestionEntry } from '@/stores/questions-store';
|
|
7
|
+
import { useTaskStore } from '@/stores/task-store';
|
|
8
|
+
import { cn } from '@/lib/utils';
|
|
9
|
+
|
|
10
|
+
interface Question {
|
|
11
|
+
question: string;
|
|
12
|
+
header: string;
|
|
13
|
+
options: Array<{ label: string; description: string }>;
|
|
14
|
+
multiSelect: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function QuestionEntryItem({ entry }: { entry: PendingQuestionEntry }) {
|
|
18
|
+
const { selectTask } = useTaskStore();
|
|
19
|
+
const { closePanel } = useQuestionsStore();
|
|
20
|
+
|
|
21
|
+
const questions = entry.questions as Question[];
|
|
22
|
+
const firstQuestion = questions[0];
|
|
23
|
+
|
|
24
|
+
const handleGoToTask = () => {
|
|
25
|
+
selectTask(entry.taskId);
|
|
26
|
+
closePanel();
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const timeAgo = formatTimeAgo(entry.timestamp);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="border-b border-border last:border-b-0">
|
|
33
|
+
{/* Clickable summary row - opens task in chat */}
|
|
34
|
+
<button
|
|
35
|
+
onClick={handleGoToTask}
|
|
36
|
+
className="w-full text-left px-4 py-3 hover:bg-accent/50 transition-colors flex items-start gap-2"
|
|
37
|
+
>
|
|
38
|
+
<div className="flex-1 min-w-0">
|
|
39
|
+
<div className="flex items-center gap-2 mb-0.5">
|
|
40
|
+
<span className="text-sm font-medium truncate">{entry.taskTitle}</span>
|
|
41
|
+
<span className="text-[10px] text-muted-foreground shrink-0">{timeAgo}</span>
|
|
42
|
+
</div>
|
|
43
|
+
<div className="flex items-center gap-1.5">
|
|
44
|
+
<Badge variant="outline" className="text-[10px] px-1 py-0">
|
|
45
|
+
{firstQuestion?.header}
|
|
46
|
+
</Badge>
|
|
47
|
+
<span className="text-xs text-muted-foreground truncate">
|
|
48
|
+
{firstQuestion?.question}
|
|
49
|
+
</span>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatTimeAgo(timestamp: number): string {
|
|
58
|
+
const diffMs = Date.now() - timestamp;
|
|
59
|
+
const diffMin = Math.floor(diffMs / 60000);
|
|
60
|
+
if (diffMin < 1) return 'just now';
|
|
61
|
+
if (diffMin < 60) return `${diffMin}m ago`;
|
|
62
|
+
const diffHr = Math.floor(diffMs / 3600000);
|
|
63
|
+
if (diffHr < 24) return `${diffHr}h ago`;
|
|
64
|
+
return `${Math.floor(diffMs / 86400000)}d ago`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface QuestionsPanelProps {
|
|
68
|
+
className?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function QuestionsPanel({ className }: QuestionsPanelProps) {
|
|
72
|
+
const { isOpen, closePanel, pendingQuestions } = useQuestionsStore();
|
|
73
|
+
const entries = Array.from(pendingQuestions.values()).sort((a, b) => b.timestamp - a.timestamp);
|
|
74
|
+
|
|
75
|
+
if (!isOpen) return null;
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<>
|
|
79
|
+
{/* Overlay for mobile */}
|
|
80
|
+
<div
|
|
81
|
+
className="fixed inset-0 bg-black/50 z-40 sm:hidden"
|
|
82
|
+
onClick={closePanel}
|
|
83
|
+
/>
|
|
84
|
+
|
|
85
|
+
{/* Sidebar */}
|
|
86
|
+
<div
|
|
87
|
+
className={cn(
|
|
88
|
+
'fixed right-0 top-0 h-full w-96 bg-background border-l shadow-lg z-50',
|
|
89
|
+
'flex flex-col',
|
|
90
|
+
className
|
|
91
|
+
)}
|
|
92
|
+
>
|
|
93
|
+
{/* Header */}
|
|
94
|
+
<div className="flex items-center justify-between px-4 py-3 border-b">
|
|
95
|
+
<div className="flex items-center gap-2">
|
|
96
|
+
<MessageCircleQuestion className="size-4 text-muted-foreground" />
|
|
97
|
+
<h2 className="font-semibold text-sm">Pending Questions</h2>
|
|
98
|
+
{entries.length > 0 && (
|
|
99
|
+
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
|
|
100
|
+
{entries.length}
|
|
101
|
+
</Badge>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
<Button
|
|
105
|
+
variant="ghost"
|
|
106
|
+
size="icon"
|
|
107
|
+
onClick={closePanel}
|
|
108
|
+
className="h-8 w-8"
|
|
109
|
+
>
|
|
110
|
+
<X className="h-4 w-4" />
|
|
111
|
+
</Button>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{/* Content */}
|
|
115
|
+
<div className="flex-1 overflow-y-auto">
|
|
116
|
+
{entries.length === 0 ? (
|
|
117
|
+
<div className="px-4 py-12 text-center">
|
|
118
|
+
<MessageCircleQuestion className="size-10 text-muted-foreground/30 mx-auto mb-3" />
|
|
119
|
+
<p className="text-sm text-muted-foreground">No pending questions</p>
|
|
120
|
+
<p className="text-xs text-muted-foreground/70 mt-1">
|
|
121
|
+
Questions from running tasks will appear here
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
) : (
|
|
125
|
+
entries.map((entry) => (
|
|
126
|
+
<QuestionEntryItem
|
|
127
|
+
key={entry.attemptId}
|
|
128
|
+
entry={entry}
|
|
129
|
+
/>
|
|
130
|
+
))
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useRef } from 'react';
|
|
4
|
+
import { useTranslations } from 'next-intl';
|
|
4
5
|
import {
|
|
5
6
|
Dialog,
|
|
6
7
|
DialogContent,
|
|
@@ -44,6 +45,9 @@ export function FolderBrowserDialog({
|
|
|
44
45
|
onSelect,
|
|
45
46
|
initialPath,
|
|
46
47
|
}: FolderBrowserDialogProps) {
|
|
48
|
+
const t = useTranslations('settings');
|
|
49
|
+
const tCommon = useTranslations('common');
|
|
50
|
+
const tSidebar = useTranslations('sidebar');
|
|
47
51
|
const [currentPath, setCurrentPath] = useState(initialPath || '');
|
|
48
52
|
const [directories, setDirectories] = useState<DirectoryEntry[]>([]);
|
|
49
53
|
const [parentPath, setParentPath] = useState<string | null>(null);
|
|
@@ -152,7 +156,7 @@ export function FolderBrowserDialog({
|
|
|
152
156
|
const handleCreate = async () => {
|
|
153
157
|
const trimmedName = createName.trim();
|
|
154
158
|
if (!trimmedName) {
|
|
155
|
-
toast.error('
|
|
159
|
+
toast.error(tSidebar('nameCannotBeEmpty'));
|
|
156
160
|
return;
|
|
157
161
|
}
|
|
158
162
|
|
|
@@ -174,12 +178,12 @@ export function FolderBrowserDialog({
|
|
|
174
178
|
throw new Error(data.error || 'Create failed');
|
|
175
179
|
}
|
|
176
180
|
|
|
177
|
-
toast.success('
|
|
181
|
+
toast.success(t('folderCreated'));
|
|
178
182
|
setCreateDialogOpen(false);
|
|
179
183
|
// Refresh directory listing
|
|
180
184
|
fetchDirectory(currentPath);
|
|
181
185
|
} catch (err) {
|
|
182
|
-
toast.error(err instanceof Error ? err.message : '
|
|
186
|
+
toast.error(err instanceof Error ? err.message : t('createFailed'));
|
|
183
187
|
} finally {
|
|
184
188
|
setIsCreating(false);
|
|
185
189
|
}
|
|
@@ -212,7 +216,7 @@ export function FolderBrowserDialog({
|
|
|
212
216
|
|
|
213
217
|
const trimmedName = renameName.trim();
|
|
214
218
|
if (!trimmedName) {
|
|
215
|
-
toast.error('
|
|
219
|
+
toast.error(tSidebar('nameCannotBeEmpty'));
|
|
216
220
|
return;
|
|
217
221
|
}
|
|
218
222
|
|
|
@@ -238,12 +242,12 @@ export function FolderBrowserDialog({
|
|
|
238
242
|
throw new Error(data.error || 'Rename failed');
|
|
239
243
|
}
|
|
240
244
|
|
|
241
|
-
toast.success('
|
|
245
|
+
toast.success(t('folderRenamed'));
|
|
242
246
|
setRenameDialogOpen(false);
|
|
243
247
|
// Refresh directory listing
|
|
244
248
|
fetchDirectory(currentPath);
|
|
245
249
|
} catch (err) {
|
|
246
|
-
toast.error(err instanceof Error ? err.message : '
|
|
250
|
+
toast.error(err instanceof Error ? err.message : t('renameFailed'));
|
|
247
251
|
} finally {
|
|
248
252
|
setIsRenaming(false);
|
|
249
253
|
}
|
|
@@ -263,9 +267,9 @@ export function FolderBrowserDialog({
|
|
|
263
267
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
264
268
|
<DialogContent className="sm:max-w-[600px] h-[600px] flex flex-col overflow-hidden">
|
|
265
269
|
<DialogHeader>
|
|
266
|
-
<DialogTitle>
|
|
270
|
+
<DialogTitle>{t('selectFolder')}</DialogTitle>
|
|
267
271
|
<DialogDescription>
|
|
268
|
-
|
|
272
|
+
{t('navigateAndSelect')}
|
|
269
273
|
</DialogDescription>
|
|
270
274
|
</DialogHeader>
|
|
271
275
|
|
|
@@ -291,7 +295,7 @@ export function FolderBrowserDialog({
|
|
|
291
295
|
disabled={!parentPath || loading}
|
|
292
296
|
>
|
|
293
297
|
<ChevronUp className="h-4 w-4 mr-1" />
|
|
294
|
-
|
|
298
|
+
{t('up')}
|
|
295
299
|
</Button>
|
|
296
300
|
<Button
|
|
297
301
|
variant="outline"
|
|
@@ -300,7 +304,7 @@ export function FolderBrowserDialog({
|
|
|
300
304
|
disabled={loading}
|
|
301
305
|
>
|
|
302
306
|
<Home className="h-4 w-4 mr-1" />
|
|
303
|
-
|
|
307
|
+
{t('home')}
|
|
304
308
|
</Button>
|
|
305
309
|
<div className="flex-1" />
|
|
306
310
|
<Button
|
|
@@ -310,7 +314,7 @@ export function FolderBrowserDialog({
|
|
|
310
314
|
disabled={loading || !currentPath}
|
|
311
315
|
>
|
|
312
316
|
<FolderPlus className="h-4 w-4 mr-1" />
|
|
313
|
-
|
|
317
|
+
{t('createNewFolder')}
|
|
314
318
|
</Button>
|
|
315
319
|
</div>
|
|
316
320
|
|
|
@@ -330,7 +334,7 @@ export function FolderBrowserDialog({
|
|
|
330
334
|
</div>
|
|
331
335
|
) : directories.length === 0 ? (
|
|
332
336
|
<div className="flex items-center justify-center h-[200px] text-muted-foreground">
|
|
333
|
-
|
|
337
|
+
{t('noSubdirectories')}
|
|
334
338
|
</div>
|
|
335
339
|
) : (
|
|
336
340
|
<div className="p-2 space-y-1">
|
|
@@ -354,7 +358,7 @@ export function FolderBrowserDialog({
|
|
|
354
358
|
e.stopPropagation();
|
|
355
359
|
openRenameDialog(dir);
|
|
356
360
|
}}
|
|
357
|
-
title=
|
|
361
|
+
title={t('renameFolderTitle')}
|
|
358
362
|
>
|
|
359
363
|
<Pencil className="h-3.5 w-3.5" />
|
|
360
364
|
</Button>
|
|
@@ -368,10 +372,10 @@ export function FolderBrowserDialog({
|
|
|
368
372
|
{/* Actions */}
|
|
369
373
|
<div className="flex justify-end gap-2 pt-2">
|
|
370
374
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
371
|
-
|
|
375
|
+
{tCommon('cancel')}
|
|
372
376
|
</Button>
|
|
373
377
|
<Button onClick={handleSelect} disabled={!currentPath}>
|
|
374
|
-
|
|
378
|
+
{t('selectThisFolder')}
|
|
375
379
|
</Button>
|
|
376
380
|
</div>
|
|
377
381
|
</DialogContent>
|
|
@@ -380,14 +384,14 @@ export function FolderBrowserDialog({
|
|
|
380
384
|
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
|
381
385
|
<DialogContent>
|
|
382
386
|
<DialogHeader>
|
|
383
|
-
<DialogTitle>
|
|
387
|
+
<DialogTitle>{t('createNewFolder')}</DialogTitle>
|
|
384
388
|
<DialogDescription>
|
|
385
|
-
|
|
389
|
+
{t('enterFolderNameIn')} <strong>{currentPath.split('/').pop() || currentPath}</strong>
|
|
386
390
|
</DialogDescription>
|
|
387
391
|
</DialogHeader>
|
|
388
392
|
<div className="space-y-4 py-4">
|
|
389
393
|
<div className="space-y-2">
|
|
390
|
-
<Label htmlFor="create-folder-name">
|
|
394
|
+
<Label htmlFor="create-folder-name">{t('folderName')}</Label>
|
|
391
395
|
<Input
|
|
392
396
|
id="create-folder-name"
|
|
393
397
|
ref={createInputRef}
|
|
@@ -405,10 +409,10 @@ export function FolderBrowserDialog({
|
|
|
405
409
|
onClick={() => setCreateDialogOpen(false)}
|
|
406
410
|
disabled={isCreating}
|
|
407
411
|
>
|
|
408
|
-
|
|
412
|
+
{tCommon('cancel')}
|
|
409
413
|
</Button>
|
|
410
414
|
<Button onClick={handleCreate} disabled={isCreating}>
|
|
411
|
-
{isCreating ? '
|
|
415
|
+
{isCreating ? tCommon('creating') : tCommon('create')}
|
|
412
416
|
</Button>
|
|
413
417
|
</DialogFooter>
|
|
414
418
|
</DialogContent>
|
|
@@ -418,14 +422,14 @@ export function FolderBrowserDialog({
|
|
|
418
422
|
<Dialog open={renameDialogOpen} onOpenChange={setRenameDialogOpen}>
|
|
419
423
|
<DialogContent>
|
|
420
424
|
<DialogHeader>
|
|
421
|
-
<DialogTitle>
|
|
425
|
+
<DialogTitle>{t('renameFolder')}</DialogTitle>
|
|
422
426
|
<DialogDescription>
|
|
423
|
-
|
|
427
|
+
{t('enterNewNameFor')} <strong>{renameTarget?.name}</strong>
|
|
424
428
|
</DialogDescription>
|
|
425
429
|
</DialogHeader>
|
|
426
430
|
<div className="space-y-4 py-4">
|
|
427
431
|
<div className="space-y-2">
|
|
428
|
-
<Label htmlFor="rename-folder-name">
|
|
432
|
+
<Label htmlFor="rename-folder-name">{t('newName')}</Label>
|
|
429
433
|
<Input
|
|
430
434
|
id="rename-folder-name"
|
|
431
435
|
ref={renameInputRef}
|
|
@@ -443,10 +447,10 @@ export function FolderBrowserDialog({
|
|
|
443
447
|
onClick={() => setRenameDialogOpen(false)}
|
|
444
448
|
disabled={isRenaming}
|
|
445
449
|
>
|
|
446
|
-
|
|
450
|
+
{tCommon('cancel')}
|
|
447
451
|
</Button>
|
|
448
452
|
<Button onClick={handleRename} disabled={isRenaming}>
|
|
449
|
-
{isRenaming ? '
|
|
453
|
+
{isRenaming ? tCommon('renaming') : tCommon('rename')}
|
|
450
454
|
</Button>
|
|
451
455
|
</DialogFooter>
|
|
452
456
|
</DialogContent>
|