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.
Files changed (86) hide show
  1. package/locales/de.json +374 -12
  2. package/locales/en.json +374 -12
  3. package/locales/es.json +398 -11
  4. package/locales/fr.json +398 -11
  5. package/locales/ja.json +398 -11
  6. package/locales/ko.json +398 -11
  7. package/locales/vi.json +374 -12
  8. package/locales/zh.json +398 -11
  9. package/package.json +1 -1
  10. package/server.ts +283 -6
  11. package/src/app/[locale]/not-found.tsx +6 -3
  12. package/src/app/[locale]/page.tsx +14 -4
  13. package/src/app/api/attempts/[id]/workflow/route.ts +76 -0
  14. package/src/app/api/questions/answer/route.ts +58 -0
  15. package/src/app/api/questions/route.ts +68 -0
  16. package/src/app/api/tasks/[id]/compact/route.ts +62 -0
  17. package/src/components/access-anywhere/api-access-key-setup-modal.tsx +2 -2
  18. package/src/components/access-anywhere/tunnel-settings-dialog.tsx +6 -6
  19. package/src/components/access-anywhere/wizard-step-ctunnel.tsx +8 -8
  20. package/src/components/agent-factory/dependency-tree.tsx +5 -3
  21. package/src/components/agent-factory/discovery-dialog.tsx +26 -22
  22. package/src/components/agent-factory/plugin-detail-dialog.tsx +41 -38
  23. package/src/components/agent-factory/plugin-form-dialog.tsx +23 -20
  24. package/src/components/agent-factory/plugin-list.tsx +20 -17
  25. package/src/components/agent-factory/upload-dialog.tsx +17 -14
  26. package/src/components/auth/agent-provider-dialog.tsx +67 -65
  27. package/src/components/auth/api-key-dialog.tsx +14 -11
  28. package/src/components/auth/auth-error-message.tsx +6 -3
  29. package/src/components/editor/code-editor-with-inline-edit.tsx +4 -2
  30. package/src/components/editor/file-diff-resolver-modal.tsx +31 -26
  31. package/src/components/editor/inline-edit-dialog.tsx +9 -6
  32. package/src/components/editor/selection-mention-popup.tsx +3 -1
  33. package/src/components/header/project-selector.tsx +7 -4
  34. package/src/components/header.tsx +70 -4
  35. package/src/components/kanban/column.tsx +11 -0
  36. package/src/components/kanban/task-card.tsx +70 -4
  37. package/src/components/project-settings/component-selector.tsx +3 -1
  38. package/src/components/project-settings/plugin-upload-dialog.tsx +7 -5
  39. package/src/components/project-settings/project-settings-dialog.tsx +5 -3
  40. package/src/components/questions/questions-panel.tsx +136 -0
  41. package/src/components/settings/folder-browser-dialog.tsx +29 -25
  42. package/src/components/settings/settings-page.tsx +64 -18
  43. package/src/components/settings/setup-dialog.tsx +26 -23
  44. package/src/components/setup/unified-setup-wizard.tsx +12 -9
  45. package/src/components/sidebar/file-browser/file-create-buttons.tsx +7 -3
  46. package/src/components/sidebar/file-browser/file-tab-content.tsx +19 -15
  47. package/src/components/sidebar/file-browser/file-tabs-panel.tsx +7 -4
  48. package/src/components/sidebar/file-browser/file-tree.tsx +3 -1
  49. package/src/components/sidebar/git-changes/branch-checkout-modal.tsx +6 -4
  50. package/src/components/sidebar/git-changes/commit-details-modal.tsx +5 -3
  51. package/src/components/sidebar/git-changes/diff-tabs-panel.tsx +3 -1
  52. package/src/components/sidebar/git-changes/git-file-item.tsx +8 -6
  53. package/src/components/sidebar/git-changes/git-graph.tsx +8 -5
  54. package/src/components/sidebar/git-changes/git-panel.tsx +28 -27
  55. package/src/components/sidebar/git-changes/git-section.tsx +5 -3
  56. package/src/components/sidebar/shells/shell-panel.tsx +3 -1
  57. package/src/components/task/attachment-bar.tsx +4 -1
  58. package/src/components/task/attempt-item.tsx +7 -5
  59. package/src/components/task/conversation-view.tsx +21 -13
  60. package/src/components/task/floating-chat-window.tsx +14 -5
  61. package/src/components/task/interactive-command/checkpoint-list.tsx +5 -3
  62. package/src/components/task/interactive-command/confirm-dialog.tsx +9 -4
  63. package/src/components/task/interactive-command/interactive-command-overlay.tsx +23 -9
  64. package/src/components/task/interactive-command/question-prompt.tsx +12 -8
  65. package/src/components/task/pending-question-indicator.tsx +5 -3
  66. package/src/components/task/prompt-input.tsx +1 -1
  67. package/src/components/task/shell-log-view.tsx +3 -1
  68. package/src/components/task/status-line.tsx +84 -23
  69. package/src/components/task/task-detail-panel.tsx +27 -27
  70. package/src/components/task/task-shell-indicator.tsx +10 -6
  71. package/src/components/terminal/terminal-context-menu.tsx +6 -4
  72. package/src/components/terminal/terminal-instance.tsx +11 -3
  73. package/src/components/terminal/terminal-panel.tsx +6 -3
  74. package/src/components/terminal/terminal-shortcut-bar.tsx +3 -1
  75. package/src/components/terminal/terminal-tab-bar.tsx +5 -3
  76. package/src/components/workflow/workflow-panel.tsx +181 -0
  77. package/src/hooks/use-attempt-stream.ts +96 -3
  78. package/src/lib/agent-manager.ts +89 -3
  79. package/src/lib/db/index.ts +18 -0
  80. package/src/lib/db/schema.ts +29 -0
  81. package/src/lib/process-manager.ts +28 -7
  82. package/src/lib/session-manager.ts +60 -0
  83. package/src/lib/usage-tracker.ts +19 -19
  84. package/src/lib/workflow-tracker.ts +118 -20
  85. package/src/stores/questions-store.ts +76 -0
  86. 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, scroll if was near bottom recently (300ms threshold)
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
- let lastNearBottomTime = Date.now();
241
- const NEAR_BOTTOM_THRESHOLD = 300; // 300ms
241
+ // Start stuck to bottom
242
+ let isStuckToBottom = true;
242
243
 
243
244
  const observer = new MutationObserver(() => {
244
- if (isNearBottom()) {
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
- // Resume auto-scroll when user scrolls back near bottom
260
- const handleScroll = () => {
261
- if (isNearBottom()) {
262
- lastNearBottomTime = Date.now();
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 viewport = contentContainer.querySelector('[data-slot="scroll-area-viewport"]');
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={() => setShowQuestionPrompt(true)}
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>Loading question...</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="Edit 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="Maximize to panel"
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('Rewound conversation & files to checkpoint', {
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('Rewound conversation only', {
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('Rewound conversation to checkpoint', {
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 = 'Confirm',
22
- cancelLabel = 'Cancel',
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
- {cancelLabel}
66
+ {resolvedCancelLabel}
62
67
  </Button>
63
68
  <Button
64
69
  variant={confirmVariant}
65
70
  size="sm"
66
71
  onClick={onConfirm}
67
72
  >
68
- {confirmLabel}
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="Clear Conversation"
95
- message="Are you sure you want to clear all messages? This cannot be undone."
96
- confirmLabel="Clear"
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="Compact Conversation"
108
- message="This will summarize the conversation to save context space. Continue?"
109
- confirmLabel="Compact"
110
- onConfirm={() => {
111
- // TODO: Implement compact
112
- closeCommand();
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)` : 'Type something.';
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
- Submit
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">Review your answers</p>
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">⚠ You have not answered all questions</p>
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">Ready to submit your answers?</p>
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">Submit answers{answeredCount > 0 ? ` (${answeredCount}/${questions.length})` : ''}</span>
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">Cancel</span>
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="Type your answer..."
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
- {questions.length} question{questions.length > 1 ? 's' : ''} pending
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
- +{questions.length - 1} more question{questions.length - 1 > 1 ? 's' : ''}
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
- Open
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('Please wait for files to finish uploading');
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="Close (Esc)"
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 WorkflowSummary {
11
- chain: string[];
12
- completedCount: number;
13
- activeCount: number;
14
- totalCount: number;
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<WorkflowSummary | null>(null);
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; workflow: WorkflowSummary }) => {
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.workflow);
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>No attempt running. Send a message to start.</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>Waiting for tracking data...</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 items-center gap-1.5">
199
- <Workflow className="size-3.5" />
200
- <span className="font-medium">
201
- {workflow.chain.slice(0, 3).join(' ')}
202
- {workflow.chain.length > 3 && ' ...'}
203
- </span>
204
- <span className="text-muted-foreground/70">
205
- ({workflow.completedCount}/{workflow.totalCount} done)
206
- </span>
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>