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
|
@@ -230,22 +230,19 @@ export function ConversationView({
|
|
|
230
230
|
}
|
|
231
231
|
}, [isRunning]);
|
|
232
232
|
|
|
233
|
-
// Auto-scroll: during streaming,
|
|
233
|
+
// Auto-scroll: during streaming, use sticky-to-bottom pattern
|
|
234
|
+
// When user scrolls up, stop auto-scrolling. Resume only when they scroll back to bottom.
|
|
234
235
|
useEffect(() => {
|
|
235
236
|
if (!isRunning) return;
|
|
236
237
|
|
|
237
238
|
const contentContainer = scrollAreaRef.current;
|
|
238
239
|
if (!contentContainer) return;
|
|
239
240
|
|
|
240
|
-
|
|
241
|
-
|
|
241
|
+
// Start stuck to bottom
|
|
242
|
+
let isStuckToBottom = true;
|
|
242
243
|
|
|
243
244
|
const observer = new MutationObserver(() => {
|
|
244
|
-
if (
|
|
245
|
-
lastNearBottomTime = Date.now();
|
|
246
|
-
}
|
|
247
|
-
const wasNearBottomRecently = Date.now() - lastNearBottomTime < NEAR_BOTTOM_THRESHOLD;
|
|
248
|
-
if (wasNearBottomRecently) {
|
|
245
|
+
if (isStuckToBottom) {
|
|
249
246
|
scrollToBottom();
|
|
250
247
|
}
|
|
251
248
|
});
|
|
@@ -256,14 +253,25 @@ export function ConversationView({
|
|
|
256
253
|
characterData: true,
|
|
257
254
|
});
|
|
258
255
|
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
256
|
+
// Track user scroll: unstick when scrolling up, re-stick when at bottom
|
|
257
|
+
let lastScrollTop = 0;
|
|
258
|
+
const handleScroll = (e: Event) => {
|
|
259
|
+
const target = e.target as HTMLElement;
|
|
260
|
+
const currentScrollTop = target.scrollTop;
|
|
261
|
+
const atBottom = target.scrollHeight - currentScrollTop - target.clientHeight < 50;
|
|
262
|
+
|
|
263
|
+
if (atBottom) {
|
|
264
|
+
// User scrolled back to bottom — re-stick
|
|
265
|
+
isStuckToBottom = true;
|
|
266
|
+
} else if (currentScrollTop < lastScrollTop) {
|
|
267
|
+
// User scrolled up — unstick
|
|
268
|
+
isStuckToBottom = false;
|
|
263
269
|
}
|
|
270
|
+
lastScrollTop = currentScrollTop;
|
|
264
271
|
};
|
|
265
272
|
|
|
266
|
-
const
|
|
273
|
+
const detachedContainer = contentContainer.closest('[data-detached-scroll-container]');
|
|
274
|
+
const viewport = detachedContainer || contentContainer.querySelector('[data-slot="scroll-area-viewport"]');
|
|
267
275
|
viewport?.addEventListener('scroll', handleScroll, { passive: true });
|
|
268
276
|
|
|
269
277
|
return () => {
|
|
@@ -42,6 +42,8 @@ interface FloatingChatWindowProps {
|
|
|
42
42
|
|
|
43
43
|
export function FloatingChatWindow({ task, zIndex, onClose, onMaximize, onFocus }: FloatingChatWindowProps) {
|
|
44
44
|
const t = useTranslations('chat');
|
|
45
|
+
const tCommon = useTranslations('common');
|
|
46
|
+
const tTask = useTranslations('task');
|
|
45
47
|
const tk = useTranslations('kanban');
|
|
46
48
|
const isMobile = useIsMobileViewport();
|
|
47
49
|
const { updateTaskStatus, setTaskChatInit, moveTaskToInProgress, pendingAutoStartTask, pendingAutoStartPrompt, pendingAutoStartFileIds, setPendingAutoStartTask, renameTask } = useTaskStore();
|
|
@@ -90,6 +92,7 @@ export function FloatingChatWindow({ task, zIndex, onClose, onMaximize, onFocus
|
|
|
90
92
|
activeQuestion,
|
|
91
93
|
answerQuestion,
|
|
92
94
|
cancelQuestion,
|
|
95
|
+
refetchQuestion,
|
|
93
96
|
} = useAttemptStream({
|
|
94
97
|
taskId: task.id,
|
|
95
98
|
onComplete: handleTaskComplete,
|
|
@@ -218,7 +221,13 @@ export function FloatingChatWindow({ task, zIndex, onClose, onMaximize, onFocus
|
|
|
218
221
|
currentFiles={isRunning ? currentAttemptFiles : undefined}
|
|
219
222
|
isRunning={isRunning}
|
|
220
223
|
activeQuestion={activeQuestion}
|
|
221
|
-
onOpenQuestion={() =>
|
|
224
|
+
onOpenQuestion={() => {
|
|
225
|
+
if (activeQuestion) {
|
|
226
|
+
setShowQuestionPrompt(true);
|
|
227
|
+
} else {
|
|
228
|
+
refetchQuestion();
|
|
229
|
+
}
|
|
230
|
+
}}
|
|
222
231
|
/>
|
|
223
232
|
</div>
|
|
224
233
|
);
|
|
@@ -227,7 +236,7 @@ export function FloatingChatWindow({ task, zIndex, onClose, onMaximize, onFocus
|
|
|
227
236
|
<>
|
|
228
237
|
<Separator />
|
|
229
238
|
<div className="relative">
|
|
230
|
-
{showQuestionPrompt ? (
|
|
239
|
+
{showQuestionPrompt && activeQuestion ? (
|
|
231
240
|
<div className="border-t bg-muted/30">
|
|
232
241
|
{activeQuestion ? (
|
|
233
242
|
<QuestionPrompt
|
|
@@ -249,7 +258,7 @@ export function FloatingChatWindow({ task, zIndex, onClose, onMaximize, onFocus
|
|
|
249
258
|
<div className="py-8 px-4 text-center">
|
|
250
259
|
<div className="inline-flex items-center gap-2 text-muted-foreground text-sm">
|
|
251
260
|
<div className="size-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
|
252
|
-
<span>
|
|
261
|
+
<span>{tTask('loadingQuestion')}</span>
|
|
253
262
|
</div>
|
|
254
263
|
</div>
|
|
255
264
|
)}
|
|
@@ -326,7 +335,7 @@ export function FloatingChatWindow({ task, zIndex, onClose, onMaximize, onFocus
|
|
|
326
335
|
onMouseDown={(e) => e.stopPropagation()}
|
|
327
336
|
className="p-0.5 hover:bg-accent rounded transition-colors shrink-0 cursor-pointer"
|
|
328
337
|
data-no-drag
|
|
329
|
-
title=
|
|
338
|
+
title={tCommon('editTitle')}
|
|
330
339
|
>
|
|
331
340
|
<Pencil className="size-3 text-muted-foreground" />
|
|
332
341
|
</button>
|
|
@@ -384,7 +393,7 @@ export function FloatingChatWindow({ task, zIndex, onClose, onMaximize, onFocus
|
|
|
384
393
|
variant="ghost"
|
|
385
394
|
size="icon-sm"
|
|
386
395
|
onClick={onMaximize}
|
|
387
|
-
title=
|
|
396
|
+
title={t('maximizeToPanel')}
|
|
388
397
|
>
|
|
389
398
|
<Maximize2 className="size-4" />
|
|
390
399
|
</Button>
|
|
@@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button';
|
|
|
6
6
|
import { useInteractiveCommandStore } from '@/stores/interactive-command-store';
|
|
7
7
|
import { cn } from '@/lib/utils';
|
|
8
8
|
import { toast } from 'sonner';
|
|
9
|
+
import { useTranslations } from 'next-intl';
|
|
9
10
|
|
|
10
11
|
interface Checkpoint {
|
|
11
12
|
id: string;
|
|
@@ -27,6 +28,7 @@ interface CheckpointListProps {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export function CheckpointList({ taskId }: CheckpointListProps) {
|
|
31
|
+
const t = useTranslations('chat');
|
|
30
32
|
const [checkpoints, setCheckpoints] = useState<Checkpoint[]>([]);
|
|
31
33
|
const [loading, setLoading] = useState(true);
|
|
32
34
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
@@ -83,17 +85,17 @@ export function CheckpointList({ taskId }: CheckpointListProps) {
|
|
|
83
85
|
|
|
84
86
|
// Determine toast type and message based on result
|
|
85
87
|
if (hasFileRewind) {
|
|
86
|
-
toast.success('
|
|
88
|
+
toast.success(t('rewoundToCheckpoint'), {
|
|
87
89
|
description: 'Files restored via SDK checkpointing'
|
|
88
90
|
});
|
|
89
91
|
} else if (selectedCheckpoint?.gitCommitHash && fileRewindError) {
|
|
90
92
|
// File checkpoint exists but rewind failed
|
|
91
|
-
toast.warning('
|
|
93
|
+
toast.warning(t('rewoundConversationOnly'), {
|
|
92
94
|
description: fileRewindError,
|
|
93
95
|
duration: 6000, // Show longer for error details
|
|
94
96
|
});
|
|
95
97
|
} else {
|
|
96
|
-
toast.success('
|
|
98
|
+
toast.success(t('rewoundConversation'), {
|
|
97
99
|
description: selectedCheckpoint?.gitCommitHash
|
|
98
100
|
? 'File rewind unavailable'
|
|
99
101
|
: 'No file checkpoint for this attempt'
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect } from 'react';
|
|
4
4
|
import { AlertTriangle } from 'lucide-react';
|
|
5
|
+
import { useTranslations } from 'next-intl';
|
|
5
6
|
import { Button } from '@/components/ui/button';
|
|
6
7
|
import { cn } from '@/lib/utils';
|
|
7
8
|
|
|
@@ -18,12 +19,16 @@ interface ConfirmDialogProps {
|
|
|
18
19
|
export function ConfirmDialog({
|
|
19
20
|
title,
|
|
20
21
|
message,
|
|
21
|
-
confirmLabel
|
|
22
|
-
cancelLabel
|
|
22
|
+
confirmLabel,
|
|
23
|
+
cancelLabel,
|
|
23
24
|
confirmVariant = 'default',
|
|
24
25
|
onConfirm,
|
|
25
26
|
onCancel,
|
|
26
27
|
}: ConfirmDialogProps) {
|
|
28
|
+
const tCommon = useTranslations('common');
|
|
29
|
+
const resolvedConfirmLabel = confirmLabel ?? tCommon('confirm');
|
|
30
|
+
const resolvedCancelLabel = cancelLabel ?? tCommon('cancel');
|
|
31
|
+
|
|
27
32
|
// Keyboard shortcuts
|
|
28
33
|
useEffect(() => {
|
|
29
34
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
@@ -58,14 +63,14 @@ export function ConfirmDialog({
|
|
|
58
63
|
|
|
59
64
|
<div className="flex items-center justify-end gap-2 mt-6">
|
|
60
65
|
<Button variant="ghost" size="sm" onClick={onCancel}>
|
|
61
|
-
{
|
|
66
|
+
{resolvedCancelLabel}
|
|
62
67
|
</Button>
|
|
63
68
|
<Button
|
|
64
69
|
variant={confirmVariant}
|
|
65
70
|
size="sm"
|
|
66
71
|
onClick={onConfirm}
|
|
67
72
|
>
|
|
68
|
-
{
|
|
73
|
+
{resolvedConfirmLabel}
|
|
69
74
|
<kbd className="ml-2 px-1.5 py-0.5 text-[10px] bg-background/20 rounded">Enter</kbd>
|
|
70
75
|
</Button>
|
|
71
76
|
</div>
|
|
@@ -14,6 +14,7 @@ import { ModelSelector } from './model-selector';
|
|
|
14
14
|
import { ConfigEditor } from './config-editor';
|
|
15
15
|
import { ConfirmDialog } from './confirm-dialog';
|
|
16
16
|
import { cn } from '@/lib/utils';
|
|
17
|
+
import { useTranslations } from 'next-intl';
|
|
17
18
|
|
|
18
19
|
// Get icon for command type
|
|
19
20
|
function getCommandIcon(command: InteractiveCommand) {
|
|
@@ -34,6 +35,8 @@ function getCommandIcon(command: InteractiveCommand) {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
export function InteractiveCommandOverlay() {
|
|
38
|
+
const t = useTranslations('chat');
|
|
39
|
+
const tCommon = useTranslations('common');
|
|
37
40
|
const { activeCommand, isOpen, isLoading, error, closeCommand } =
|
|
38
41
|
useInteractiveCommandStore();
|
|
39
42
|
|
|
@@ -91,9 +94,9 @@ export function InteractiveCommandOverlay() {
|
|
|
91
94
|
)}
|
|
92
95
|
{activeCommand.type === 'clear' && (
|
|
93
96
|
<ConfirmDialog
|
|
94
|
-
title=
|
|
95
|
-
message=
|
|
96
|
-
confirmLabel=
|
|
97
|
+
title={t('clearConversation')}
|
|
98
|
+
message={t('clearConversationConfirm')}
|
|
99
|
+
confirmLabel={tCommon('clear')}
|
|
97
100
|
confirmVariant="destructive"
|
|
98
101
|
onConfirm={() => {
|
|
99
102
|
// TODO: Implement clear
|
|
@@ -104,12 +107,23 @@ export function InteractiveCommandOverlay() {
|
|
|
104
107
|
)}
|
|
105
108
|
{activeCommand.type === 'compact' && (
|
|
106
109
|
<ConfirmDialog
|
|
107
|
-
title=
|
|
108
|
-
message=
|
|
109
|
-
confirmLabel=
|
|
110
|
-
onConfirm={() => {
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
title={t('compactConversation')}
|
|
111
|
+
message={t('compactConversationConfirm')}
|
|
112
|
+
confirmLabel={t('compact')}
|
|
113
|
+
onConfirm={async () => {
|
|
114
|
+
const { setLoading, setError } = useInteractiveCommandStore.getState();
|
|
115
|
+
setLoading(true);
|
|
116
|
+
try {
|
|
117
|
+
const res = await fetch(`/api/tasks/${activeCommand.taskId}/compact`, { method: 'POST' });
|
|
118
|
+
if (!res.ok) {
|
|
119
|
+
const data = await res.json();
|
|
120
|
+
setError(data.error || 'Failed to compact');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
closeCommand();
|
|
124
|
+
} catch (err) {
|
|
125
|
+
setError('Failed to compact conversation');
|
|
126
|
+
}
|
|
113
127
|
}}
|
|
114
128
|
onCancel={closeCommand}
|
|
115
129
|
/>
|
|
@@ -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 { cn } from '@/lib/utils';
|
|
5
6
|
|
|
6
7
|
interface QuestionOption {
|
|
@@ -22,6 +23,9 @@ interface QuestionPromptProps {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export function QuestionPrompt({ questions, onAnswer, onCancel }: QuestionPromptProps) {
|
|
26
|
+
const t = useTranslations('task');
|
|
27
|
+
const tChat = useTranslations('chat');
|
|
28
|
+
const tCommon = useTranslations('common');
|
|
25
29
|
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
|
|
26
30
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
27
31
|
const [selectedMulti, setSelectedMulti] = useState<Set<number>>(new Set());
|
|
@@ -51,7 +55,7 @@ export function QuestionPrompt({ questions, onAnswer, onCancel }: QuestionPrompt
|
|
|
51
55
|
// Add "Type something" as last option (like "Other" in Claude)
|
|
52
56
|
// If user already typed a custom answer, show it instead of "Type something."
|
|
53
57
|
const existingCustom = getCustomAnswer(currentQuestionIndex);
|
|
54
|
-
const typeOptionLabel = existingCustom ? `${existingCustom} (edit)` : '
|
|
58
|
+
const typeOptionLabel = existingCustom ? `${existingCustom} (edit)` : t('typeSomething');
|
|
55
59
|
const allOptions = [...currentQuestion.options, { label: typeOptionLabel, description: '' }];
|
|
56
60
|
const isLastOption = selectedIndex === allOptions.length - 1;
|
|
57
61
|
|
|
@@ -315,7 +319,7 @@ export function QuestionPrompt({ questions, onAnswer, onCancel }: QuestionPrompt
|
|
|
315
319
|
<span className="text-[10px]">
|
|
316
320
|
{allAnswered ? '✓' : '□'}
|
|
317
321
|
</span>
|
|
318
|
-
|
|
322
|
+
{tCommon('submit')}
|
|
319
323
|
</button>
|
|
320
324
|
|
|
321
325
|
{/* Forward arrow (hidden for single question) */}
|
|
@@ -352,13 +356,13 @@ export function QuestionPrompt({ questions, onAnswer, onCancel }: QuestionPrompt
|
|
|
352
356
|
<>
|
|
353
357
|
{/* Review header */}
|
|
354
358
|
<div className="px-4 mb-3">
|
|
355
|
-
<p className="text-sm font-bold">
|
|
359
|
+
<p className="text-sm font-bold">{t('reviewAnswers')}</p>
|
|
356
360
|
</div>
|
|
357
361
|
|
|
358
362
|
{/* Warning if not all answered */}
|
|
359
363
|
{!allAnswered && (
|
|
360
364
|
<div className="px-4 mb-3">
|
|
361
|
-
<p className="text-sm text-yellow-500">⚠
|
|
365
|
+
<p className="text-sm text-yellow-500">⚠ {t('notAllAnswered')}</p>
|
|
362
366
|
</div>
|
|
363
367
|
)}
|
|
364
368
|
|
|
@@ -392,7 +396,7 @@ export function QuestionPrompt({ questions, onAnswer, onCancel }: QuestionPrompt
|
|
|
392
396
|
|
|
393
397
|
{/* Submit prompt */}
|
|
394
398
|
<div className="px-4 mb-3">
|
|
395
|
-
<p className="text-sm text-muted-foreground">
|
|
399
|
+
<p className="text-sm text-muted-foreground">{t('readyToSubmit')}</p>
|
|
396
400
|
</div>
|
|
397
401
|
|
|
398
402
|
{/* Submit / Cancel options */}
|
|
@@ -410,7 +414,7 @@ export function QuestionPrompt({ questions, onAnswer, onCancel }: QuestionPrompt
|
|
|
410
414
|
{selectedIndex === 0 ? '›' : ' '}
|
|
411
415
|
</span>
|
|
412
416
|
<span className="shrink-0 text-sm text-muted-foreground">1.</span>
|
|
413
|
-
<span className="text-sm font-medium">
|
|
417
|
+
<span className="text-sm font-medium">{t('submitAnswers')}{answeredCount > 0 ? ` (${answeredCount}/${questions.length})` : ''}</span>
|
|
414
418
|
</button>
|
|
415
419
|
<button
|
|
416
420
|
onClick={() => onCancel()}
|
|
@@ -424,7 +428,7 @@ export function QuestionPrompt({ questions, onAnswer, onCancel }: QuestionPrompt
|
|
|
424
428
|
{selectedIndex === 1 ? '›' : ' '}
|
|
425
429
|
</span>
|
|
426
430
|
<span className="shrink-0 text-sm text-muted-foreground">2.</span>
|
|
427
|
-
<span className="text-sm font-medium">
|
|
431
|
+
<span className="text-sm font-medium">{tCommon('cancel')}</span>
|
|
428
432
|
</button>
|
|
429
433
|
</div>
|
|
430
434
|
</>
|
|
@@ -537,7 +541,7 @@ export function QuestionPrompt({ questions, onAnswer, onCancel }: QuestionPrompt
|
|
|
537
541
|
type="text"
|
|
538
542
|
value={customInput}
|
|
539
543
|
onChange={(e) => setCustomInput(e.target.value)}
|
|
540
|
-
placeholder=
|
|
544
|
+
placeholder={tChat('typeYourAnswer')}
|
|
541
545
|
className="w-full px-3 py-2 text-sm border rounded bg-background focus:outline-none focus:ring-2 focus:ring-primary"
|
|
542
546
|
autoFocus
|
|
543
547
|
/>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { ExternalLink } from 'lucide-react';
|
|
4
|
+
import { useTranslations } from 'next-intl';
|
|
4
5
|
import { Button } from '@/components/ui/button';
|
|
5
6
|
import { Badge } from '@/components/ui/badge';
|
|
6
7
|
|
|
@@ -17,6 +18,7 @@ interface PendingQuestionIndicatorProps {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export function PendingQuestionIndicator({ questions, onOpen }: PendingQuestionIndicatorProps) {
|
|
21
|
+
const t = useTranslations('task');
|
|
20
22
|
const firstQuestion = questions[0];
|
|
21
23
|
|
|
22
24
|
return (
|
|
@@ -30,7 +32,7 @@ export function PendingQuestionIndicator({ questions, onOpen }: PendingQuestionI
|
|
|
30
32
|
{firstQuestion.header}
|
|
31
33
|
</Badge>
|
|
32
34
|
<span className="text-xs text-muted-foreground">
|
|
33
|
-
{
|
|
35
|
+
{t('questionsPending', { count: questions.length })}
|
|
34
36
|
</span>
|
|
35
37
|
</div>
|
|
36
38
|
<p className="text-sm text-foreground truncate">
|
|
@@ -38,7 +40,7 @@ export function PendingQuestionIndicator({ questions, onOpen }: PendingQuestionI
|
|
|
38
40
|
</p>
|
|
39
41
|
{questions.length > 1 && (
|
|
40
42
|
<p className="text-xs text-muted-foreground mt-1">
|
|
41
|
-
|
|
43
|
+
{t('moreQuestions', { count: questions.length - 1 })}
|
|
42
44
|
</p>
|
|
43
45
|
)}
|
|
44
46
|
</div>
|
|
@@ -49,7 +51,7 @@ export function PendingQuestionIndicator({ questions, onOpen }: PendingQuestionI
|
|
|
49
51
|
onClick={onOpen}
|
|
50
52
|
className="shrink-0"
|
|
51
53
|
>
|
|
52
|
-
|
|
54
|
+
{t('open')}
|
|
53
55
|
</Button>
|
|
54
56
|
</div>
|
|
55
57
|
</div>
|
|
@@ -291,7 +291,7 @@ export const PromptInput = forwardRef<PromptInputRef, PromptInputProps>(({
|
|
|
291
291
|
|
|
292
292
|
// Check if files are still uploading
|
|
293
293
|
if (taskId && hasUploadingFiles(taskId)) {
|
|
294
|
-
toast.error('
|
|
294
|
+
toast.error(t('waitForUpload'));
|
|
295
295
|
return;
|
|
296
296
|
}
|
|
297
297
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useRef } from 'react';
|
|
4
4
|
import { X } from 'lucide-react';
|
|
5
|
+
import { useTranslations } from 'next-intl';
|
|
5
6
|
import { Button } from '@/components/ui/button';
|
|
6
7
|
import { cn } from '@/lib/utils';
|
|
7
8
|
import { useShellStore, type ShellInfo } from '@/stores/shell-store';
|
|
@@ -13,6 +14,7 @@ interface ShellLogViewProps {
|
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export function ShellLogView({ shell, onClose, className }: ShellLogViewProps) {
|
|
17
|
+
const tCommon = useTranslations('common');
|
|
16
18
|
const { shellLogs, getShellLogs } = useShellStore();
|
|
17
19
|
const logContainerRef = useRef<HTMLDivElement>(null);
|
|
18
20
|
const logs = shellLogs.get(shell.shellId) || [];
|
|
@@ -48,7 +50,7 @@ export function ShellLogView({ shell, onClose, className }: ShellLogViewProps) {
|
|
|
48
50
|
size="icon"
|
|
49
51
|
className="h-6 w-6 shrink-0"
|
|
50
52
|
onClick={onClose}
|
|
51
|
-
title=
|
|
53
|
+
title={tCommon('close') + ' (Esc)'}
|
|
52
54
|
>
|
|
53
55
|
<X className="h-3 w-3" />
|
|
54
56
|
</Button>
|
|
@@ -2,16 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from 'react';
|
|
4
4
|
import { Clock, TrendingUp, GitBranch, Workflow, Gauge } from 'lucide-react';
|
|
5
|
+
import { useTranslations } from 'next-intl';
|
|
5
6
|
import { useSocket } from '@/hooks/use-socket';
|
|
6
7
|
import { cn } from '@/lib/utils';
|
|
7
8
|
import type { UsageStats } from '@/lib/usage-tracker';
|
|
8
9
|
import type { GitStats } from '@/lib/git-stats-collector';
|
|
9
10
|
|
|
10
|
-
interface
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
interface SubagentNodeClient {
|
|
12
|
+
id: string;
|
|
13
|
+
type: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
status: 'pending' | 'in_progress' | 'completed' | 'failed' | 'orphaned';
|
|
16
|
+
parentId: string | null;
|
|
17
|
+
depth: number;
|
|
18
|
+
teamName?: string;
|
|
19
|
+
startedAt?: number;
|
|
20
|
+
completedAt?: number;
|
|
21
|
+
durationMs?: number;
|
|
22
|
+
error?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface AgentMessageClient {
|
|
26
|
+
fromType: string;
|
|
27
|
+
toType: string;
|
|
28
|
+
content: string;
|
|
29
|
+
summary: string;
|
|
30
|
+
timestamp: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface WorkflowData {
|
|
34
|
+
nodes: SubagentNodeClient[];
|
|
35
|
+
messages: AgentMessageClient[];
|
|
36
|
+
summary: {
|
|
37
|
+
chain: string[];
|
|
38
|
+
completedCount: number;
|
|
39
|
+
activeCount: number;
|
|
40
|
+
totalCount: number;
|
|
41
|
+
};
|
|
15
42
|
}
|
|
16
43
|
|
|
17
44
|
interface StatusLineProps {
|
|
@@ -33,10 +60,12 @@ interface StatusLineProps {
|
|
|
33
60
|
* - Last completed attempt (if no running attempt)
|
|
34
61
|
*/
|
|
35
62
|
export function StatusLine({ taskId, currentAttemptId, className }: StatusLineProps) {
|
|
63
|
+
const t = useTranslations('task');
|
|
36
64
|
const socket = useSocket();
|
|
37
65
|
const [usage, setUsage] = useState<UsageStats | null>(null);
|
|
38
66
|
const [gitStats, setGitStats] = useState<GitStats | null>(null);
|
|
39
|
-
const [workflow, setWorkflow] = useState<
|
|
67
|
+
const [workflow, setWorkflow] = useState<WorkflowData | null>(null);
|
|
68
|
+
const [workflowExpanded, setWorkflowExpanded] = useState(false);
|
|
40
69
|
|
|
41
70
|
// Reset state when task changes (not when attemptId changes)
|
|
42
71
|
useEffect(() => {
|
|
@@ -63,10 +92,10 @@ export function StatusLine({ taskId, currentAttemptId, className }: StatusLinePr
|
|
|
63
92
|
};
|
|
64
93
|
|
|
65
94
|
// Workflow updates
|
|
66
|
-
const handleWorkflowUpdate = (data: { attemptId: string;
|
|
95
|
+
const handleWorkflowUpdate = (data: { attemptId: string; nodes: SubagentNodeClient[]; messages: AgentMessageClient[]; summary: WorkflowData['summary'] }) => {
|
|
67
96
|
console.log('[StatusLine] Workflow update:', data);
|
|
68
97
|
if (data.attemptId === currentAttemptId) {
|
|
69
|
-
setWorkflow(data.
|
|
98
|
+
setWorkflow({ nodes: data.nodes, messages: data.messages, summary: data.summary });
|
|
70
99
|
}
|
|
71
100
|
};
|
|
72
101
|
|
|
@@ -105,14 +134,14 @@ export function StatusLine({ taskId, currentAttemptId, className }: StatusLinePr
|
|
|
105
134
|
{!hasRunningAttempt && !hasData && (
|
|
106
135
|
<div className="flex items-center gap-1.5 text-muted-foreground/50">
|
|
107
136
|
<TrendingUp className="size-3.5" />
|
|
108
|
-
<span>
|
|
137
|
+
<span>{t('noAttemptRunning')}</span>
|
|
109
138
|
</div>
|
|
110
139
|
)}
|
|
111
140
|
|
|
112
141
|
{hasRunningAttempt && !hasData && (
|
|
113
142
|
<div className="flex items-center gap-1.5 text-muted-foreground/50">
|
|
114
143
|
<TrendingUp className="size-3.5 animate-pulse" />
|
|
115
|
-
<span>
|
|
144
|
+
<span>{t('waitingForTracking')}</span>
|
|
116
145
|
</div>
|
|
117
146
|
)}
|
|
118
147
|
|
|
@@ -157,7 +186,7 @@ export function StatusLine({ taskId, currentAttemptId, className }: StatusLinePr
|
|
|
157
186
|
<div className="flex items-center gap-1.5">
|
|
158
187
|
<TrendingUp className="size-3.5" />
|
|
159
188
|
<span className="font-medium">
|
|
160
|
-
{usage.totalTokens.toLocaleString()} tokens
|
|
189
|
+
{usage.totalTokens.toLocaleString()} {t('tokens')}
|
|
161
190
|
</span>
|
|
162
191
|
{usage.totalCostUSD > 0 && (
|
|
163
192
|
<span className="text-muted-foreground/70">
|
|
@@ -166,7 +195,7 @@ export function StatusLine({ taskId, currentAttemptId, className }: StatusLinePr
|
|
|
166
195
|
)}
|
|
167
196
|
{usage.numTurns > 0 && (
|
|
168
197
|
<span className="text-muted-foreground/70">
|
|
169
|
-
· {usage.numTurns} {usage.numTurns === 1 ? 'turn' : 'turns'}
|
|
198
|
+
· {usage.numTurns} {usage.numTurns === 1 ? t('turn') : t('turns')}
|
|
170
199
|
</span>
|
|
171
200
|
)}
|
|
172
201
|
{usage.durationMs > 0 && (
|
|
@@ -188,22 +217,54 @@ export function StatusLine({ taskId, currentAttemptId, className }: StatusLinePr
|
|
|
188
217
|
-{gitStats.deletions}
|
|
189
218
|
</span>
|
|
190
219
|
<span className="text-muted-foreground/70">
|
|
191
|
-
({gitStats.filesChanged} {gitStats.filesChanged === 1 ? 'file' : 'files'})
|
|
220
|
+
({gitStats.filesChanged} {gitStats.filesChanged === 1 ? t('file') : t('files')})
|
|
192
221
|
</span>
|
|
193
222
|
</div>
|
|
194
223
|
)}
|
|
195
224
|
|
|
196
225
|
{/* Workflow Section */}
|
|
197
|
-
{workflow && workflow.totalCount > 0 && (
|
|
198
|
-
<div className="flex
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
{
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
226
|
+
{workflow && workflow.summary.totalCount > 0 && (
|
|
227
|
+
<div className="flex flex-col">
|
|
228
|
+
<div
|
|
229
|
+
className="flex items-center gap-1.5 cursor-pointer select-none"
|
|
230
|
+
onClick={() => setWorkflowExpanded(!workflowExpanded)}
|
|
231
|
+
>
|
|
232
|
+
<Workflow className="size-3.5" />
|
|
233
|
+
<span className="font-medium">
|
|
234
|
+
{workflowExpanded ? '▼' : '▶'} Workflow{workflowExpanded ? ` (${workflow.summary.totalCount} agents)` : `: ${workflow.summary.totalCount} agents (${workflow.summary.completedCount} done${workflow.summary.activeCount > 0 ? `, ${workflow.summary.activeCount} running` : ''})`}
|
|
235
|
+
</span>
|
|
236
|
+
</div>
|
|
237
|
+
{workflowExpanded && (
|
|
238
|
+
<div className="mt-1 ml-5 font-mono">
|
|
239
|
+
{workflow.nodes.map((node) => (
|
|
240
|
+
<div
|
|
241
|
+
key={node.id}
|
|
242
|
+
className="flex items-center gap-2"
|
|
243
|
+
style={{ paddingLeft: `${node.depth * 16}px` }}
|
|
244
|
+
>
|
|
245
|
+
<span className={cn(
|
|
246
|
+
node.status === 'completed' && 'text-green-500',
|
|
247
|
+
node.status === 'in_progress' && 'text-blue-500 animate-pulse',
|
|
248
|
+
node.status === 'failed' && 'text-red-500',
|
|
249
|
+
node.status === 'orphaned' && 'text-yellow-500'
|
|
250
|
+
)}>
|
|
251
|
+
{node.status === 'completed' && '✓'}
|
|
252
|
+
{node.status === 'in_progress' && '●'}
|
|
253
|
+
{node.status === 'failed' && '✗'}
|
|
254
|
+
{node.status === 'orphaned' && '⊘'}
|
|
255
|
+
{node.status === 'pending' && '○'}
|
|
256
|
+
</span>
|
|
257
|
+
<span className="font-medium">{node.name || node.type}</span>
|
|
258
|
+
<span className="text-muted-foreground/70">
|
|
259
|
+
{node.status === 'in_progress' && 'running...'}
|
|
260
|
+
{node.status === 'completed' && node.durationMs != null && formatDuration(node.durationMs)}
|
|
261
|
+
{node.status === 'failed' && 'failed'}
|
|
262
|
+
{node.status === 'orphaned' && 'orphaned'}
|
|
263
|
+
</span>
|
|
264
|
+
</div>
|
|
265
|
+
))}
|
|
266
|
+
</div>
|
|
267
|
+
)}
|
|
207
268
|
</div>
|
|
208
269
|
)}
|
|
209
270
|
</div>
|