From bf6fd85006115c6a50d740b395bed4ad45240357 Mon Sep 17 00:00:00 2001 From: oxoxDev <164490987+oxoxDev@users.noreply.github.com> Date: Tue, 16 Jun 2026 08:11:26 +0530 Subject: [PATCH] feat(intelligence): worktree isolation frontend + RPC wiring (#3376) (#3696) Co-authored-by: Steven Enamakel --- .../IntelligenceWorktreesTab.test.tsx | 67 +++ .../intelligence/IntelligenceWorktreesTab.tsx | 169 ++++++ .../worktree/WorktreeActions.test.tsx | 95 +++ .../components/worktree/WorktreeActions.tsx | 196 ++++++ app/src/lib/i18n/ar.ts | 29 + app/src/lib/i18n/bn.ts | 29 + app/src/lib/i18n/de.ts | 30 + app/src/lib/i18n/en.ts | 30 + app/src/lib/i18n/es.ts | 30 + app/src/lib/i18n/fr.ts | 29 + app/src/lib/i18n/hi.ts | 29 + app/src/lib/i18n/id.ts | 29 + app/src/lib/i18n/it.ts | 29 + app/src/lib/i18n/ko.ts | 29 + app/src/lib/i18n/pl.ts | 30 + app/src/lib/i18n/pt.ts | 29 + app/src/lib/i18n/ru.ts | 30 + app/src/lib/i18n/zh-CN.ts | 26 + app/src/pages/Intelligence.tsx | 10 + .../components/ToolTimelineBlock.tsx | 36 ++ .../__tests__/ToolTimelineBlock.test.tsx | 28 + app/src/providers/ChatRuntimeProvider.tsx | 6 + app/src/services/api/worktreeApi.test.ts | 67 +++ app/src/services/api/worktreeApi.ts | 92 +++ app/src/services/chatService.ts | 10 + app/src/utils/pathUtils.ts | 18 + src/core/all.rs | 5 + src/core/socketio.rs | 16 + src/openhuman/about_app/catalog_data.rs | 10 + src/openhuman/agent/progress.rs | 12 + src/openhuman/agent_orchestration/mod.rs | 5 + src/openhuman/agent_orchestration/ops.rs | 3 + .../tools/continue_subagent.rs | 3 + .../tools/spawn_async_subagent.rs | 3 + .../tools/spawn_parallel_agents.rs | 6 + .../tools/spawn_subagent.rs | 3 + .../agent_orchestration/worktree_schemas.rs | 556 ++++++++++++++++++ .../channels/providers/web/progress_bridge.rs | 71 ++- .../threads/turn_state/mirror_tests.rs | 3 + tests/memory_core_threads_raw_coverage_e2e.rs | 3 + 40 files changed, 1899 insertions(+), 2 deletions(-) create mode 100644 app/src/components/intelligence/IntelligenceWorktreesTab.test.tsx create mode 100644 app/src/components/intelligence/IntelligenceWorktreesTab.tsx create mode 100644 app/src/components/worktree/WorktreeActions.test.tsx create mode 100644 app/src/components/worktree/WorktreeActions.tsx create mode 100644 app/src/services/api/worktreeApi.test.ts create mode 100644 app/src/services/api/worktreeApi.ts create mode 100644 app/src/utils/pathUtils.ts create mode 100644 src/openhuman/agent_orchestration/worktree_schemas.rs diff --git a/app/src/components/intelligence/IntelligenceWorktreesTab.test.tsx b/app/src/components/intelligence/IntelligenceWorktreesTab.test.tsx new file mode 100644 index 000000000..0888b5a57 --- /dev/null +++ b/app/src/components/intelligence/IntelligenceWorktreesTab.test.tsx @@ -0,0 +1,67 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { worktreeApi, type WorktreeListView } from '../../services/api/worktreeApi'; +import IntelligenceWorktreesTab from './IntelligenceWorktreesTab'; + +vi.mock('../../services/api/worktreeApi', () => ({ + worktreeApi: { list: vi.fn(), diff: vi.fn(), remove: vi.fn() }, +})); +vi.mock('../../utils/openUrl', () => ({ revealPath: vi.fn().mockResolvedValue(undefined) })); +vi.mock('../../lib/i18n/I18nContext', () => ({ useT: () => ({ t: (k: string) => k }) })); + +const mockList = vi.mocked(worktreeApi.list); + +function view(overrides?: Partial): WorktreeListView { + return { + worktrees: [ + { + path: '/r/.claude/worktrees/worker-a', + branch: 'worker/a', + isDirty: true, + changedFiles: ['src/lib.rs', 'a.rs'], + }, + ], + overlaps: [], + ...overrides, + }; +} + +describe('IntelligenceWorktreesTab', () => { + beforeEach(() => { + mockList.mockReset(); + }); + + it('renders empty state when there are no worktrees', async () => { + mockList.mockResolvedValue({ worktrees: [], overlaps: [] }); + render(); + await waitFor(() => expect(screen.getByText('worktree.panel.empty')).toBeInTheDocument()); + }); + + it('renders a worktree row with branch, dirty badge and changed files', async () => { + mockList.mockResolvedValue(view()); + render(); + await waitFor(() => expect(screen.getByTestId('worktree-row')).toBeInTheDocument()); + expect(screen.getByText('worker/a')).toBeInTheDocument(); + expect(screen.getByText('worktree.dirty')).toBeInTheDocument(); + expect(screen.getByText('src/lib.rs')).toBeInTheDocument(); + expect(screen.getByTestId('worktree-actions')).toBeInTheDocument(); + }); + + it('renders an overlap banner when workers touched the same file', async () => { + mockList.mockResolvedValue( + view({ overlaps: [{ file: 'src/lib.rs', branches: ['worker/a', 'worker/b'] }] }) + ); + render(); + await waitFor(() => expect(screen.getByTestId('worktree-overlaps')).toBeInTheDocument()); + expect(screen.getByText('worktree.panel.overlapsTitle')).toBeInTheDocument(); + }); + + it('surfaces a load error', async () => { + mockList.mockRejectedValue(new Error('boom')); + render(); + await waitFor(() => + expect(screen.getByText(/worktree.panel.failedToLoad/)).toBeInTheDocument() + ); + }); +}); diff --git a/app/src/components/intelligence/IntelligenceWorktreesTab.tsx b/app/src/components/intelligence/IntelligenceWorktreesTab.tsx new file mode 100644 index 000000000..f6b21edc6 --- /dev/null +++ b/app/src/components/intelligence/IntelligenceWorktreesTab.tsx @@ -0,0 +1,169 @@ +/** + * IntelligenceWorktreesTab — the isolated-worktree command-center surface (#3376). + * + * Reads `openhuman.worktree_list` (via {@link worktreeApi}) and renders every + * managed worker `git worktree` (those under `/.claude/worktrees`) with + * its branch, dirty status, and changed files, plus per-row open / diff / + * remove actions. A cross-worktree overlap banner warns when two workers + * changed the same file so the user reconciles before merging. + * + * Mirrors {@link IntelligenceAgentWorkTab}'s mount pattern (mountedRef + a 0ms + * `setTimeout` so the first paint shows the loading state). Removing a worktree + * refetches the list so the row drops and overlaps recompute. + */ +import debug from 'debug'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { useT } from '../../lib/i18n/I18nContext'; +import { worktreeApi, type WorktreeListView } from '../../services/api/worktreeApi'; +import { basename } from '../../utils/pathUtils'; +import WorktreeActions from '../worktree/WorktreeActions'; + +const log = debug('intelligence:worktrees'); + +export default function IntelligenceWorktreesTab() { + const { t } = useT(); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const mountedRef = useRef(true); + + const fetchWorktrees = useCallback(async () => { + log('fetchWorktrees: entry'); + setError(null); + try { + const view = await worktreeApi.list(); + if (mountedRef.current) { + setData(view); + log('fetchWorktrees: done count=%d', view.worktrees.length); + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + log('fetchWorktrees: error %s', msg); + if (mountedRef.current) setError(msg); + } finally { + if (mountedRef.current) setLoading(false); + } + }, []); + + useEffect(() => { + mountedRef.current = true; + const handle = window.setTimeout(() => void fetchWorktrees(), 0); + return () => { + window.clearTimeout(handle); + mountedRef.current = false; + }; + }, [fetchWorktrees]); + + const onRemoved = useCallback(() => { + void fetchWorktrees(); + }, [fetchWorktrees]); + + if (loading) { + return ( +
+
+ {t('worktree.panel.loading')} +
+ ); + } + + if (error) { + return ( +
+ {t('worktree.panel.failedToLoad')}: {error} +
+ ); + } + + if (!data || data.worktrees.length === 0) { + return ( +
+

+ {t('worktree.panel.subtitle')} +

+
+ {t('worktree.panel.empty')} +
+
+ ); + } + + return ( +
+

{t('worktree.panel.subtitle')}

+ + {data.overlaps.length > 0 ? ( +
+

{t('worktree.panel.overlapsTitle')}

+

{t('worktree.panel.overlapHint')}

+
    + {data.overlaps.map(o => ( +
  • + {o.file} + + ({o.branches.join(', ')}) + +
  • + ))} +
+
+ ) : null} + +
    + {data.worktrees.map(wt => ( +
  • +
    + + {basename(wt.path)} + + {wt.branch ? ( + + {wt.branch} + + ) : null} + {wt.isDirty ? ( + + {t('worktree.dirty')} + + ) : ( + + {t('worktree.clean')} + + )} + {wt.changedFiles.length > 0 ? ( + + {wt.changedFiles.length}{' '} + {wt.changedFiles.length === 1 + ? t('worktree.changedFile') + : t('worktree.changedFiles')} + + ) : null} +
    + + {wt.changedFiles.length > 0 ? ( +
      + {wt.changedFiles.slice(0, 8).map(f => ( +
    • + {f} +
    • + ))} + {wt.changedFiles.length > 8 ? ( +
    • + +{wt.changedFiles.length - 8} +
    • + ) : null} +
    + ) : null} + + +
  • + ))} +
+
+ ); +} diff --git a/app/src/components/worktree/WorktreeActions.test.tsx b/app/src/components/worktree/WorktreeActions.test.tsx new file mode 100644 index 000000000..82ac94340 --- /dev/null +++ b/app/src/components/worktree/WorktreeActions.test.tsx @@ -0,0 +1,95 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { worktreeApi } from '../../services/api/worktreeApi'; +import { revealPath } from '../../utils/openUrl'; +import WorktreeActions from './WorktreeActions'; + +vi.mock('../../services/api/worktreeApi', () => ({ + worktreeApi: { diff: vi.fn(), remove: vi.fn() }, +})); +vi.mock('../../utils/openUrl', () => ({ revealPath: vi.fn().mockResolvedValue(undefined) })); +vi.mock('../../lib/i18n/I18nContext', () => ({ useT: () => ({ t: (k: string) => k }) })); + +const mockDiff = vi.mocked(worktreeApi.diff); +const mockRemove = vi.mocked(worktreeApi.remove); +const mockReveal = vi.mocked(revealPath); + +const PATH = '/r/.claude/worktrees/worker-a'; + +describe('WorktreeActions', () => { + beforeEach(() => { + mockDiff.mockReset(); + mockRemove.mockReset().mockResolvedValue(true); + mockReveal.mockReset().mockResolvedValue(undefined); + }); + + it('open reveals the path in the OS file manager', async () => { + const user = userEvent.setup(); + render(); + await user.click(screen.getByTestId('worktree-open')); + expect(mockReveal).toHaveBeenCalledWith(PATH); + }); + + it('diff fetches once and renders the summary, toggling closed on second click', async () => { + mockDiff.mockResolvedValue(' src/a.rs | 2 +-'); + const user = userEvent.setup(); + render(); + await user.click(screen.getByTestId('worktree-diff')); + await waitFor(() => + expect(screen.getByTestId('worktree-diff-output')).toHaveTextContent('src/a.rs') + ); + expect(mockDiff).toHaveBeenCalledTimes(1); + await user.click(screen.getByTestId('worktree-diff')); + expect(screen.queryByTestId('worktree-diff-output')).toBeNull(); + }); + + it('removes a clean worktree in one click (force=false)', async () => { + const onRemoved = vi.fn(); + const user = userEvent.setup(); + render(); + await user.click(screen.getByTestId('worktree-remove')); + await waitFor(() => expect(mockRemove).toHaveBeenCalledWith(PATH, false)); + expect(onRemoved).toHaveBeenCalledWith(PATH); + }); + + it('a dirty worktree requires explicit confirmation before removal', async () => { + const onRemoved = vi.fn(); + const user = userEvent.setup(); + render(); + await user.click(screen.getByTestId('worktree-remove')); + // No removal yet — a confirm row appears instead. + expect(mockRemove).not.toHaveBeenCalled(); + expect(screen.getByTestId('worktree-remove-confirm')).toBeInTheDocument(); + // Discard & remove forces. + await user.click(screen.getByTestId('worktree-remove-confirm-yes')); + await waitFor(() => expect(mockRemove).toHaveBeenCalledWith(PATH, true)); + expect(onRemoved).toHaveBeenCalledWith(PATH); + }); + + it('resets the removing state after success when no onRemoved parent drops the row', async () => { + // Inline timeline use (ToolTimelineBlock) renders without onRemoved, so the + // component is never unmounted by a parent — it must reset its own state or + // the button stays stuck on "worktree.removing" with disabled forever. + const user = userEvent.setup(); + render(); + await user.click(screen.getByTestId('worktree-remove')); + await waitFor(() => expect(mockRemove).toHaveBeenCalledWith(PATH, false)); + await waitFor(() => { + const btn = screen.getByTestId('worktree-remove'); + expect(btn).toHaveTextContent('worktree.action.remove'); + expect(btn).not.toBeDisabled(); + }); + }); + + it('preserve cancels the dirty-removal confirm without removing', async () => { + const user = userEvent.setup(); + render(); + await user.click(screen.getByTestId('worktree-remove')); + await user.click(screen.getByTestId('worktree-preserve')); + expect(mockRemove).not.toHaveBeenCalled(); + expect(screen.queryByTestId('worktree-remove-confirm')).toBeNull(); + expect(screen.getByTestId('worktree-remove')).toBeInTheDocument(); + }); +}); diff --git a/app/src/components/worktree/WorktreeActions.tsx b/app/src/components/worktree/WorktreeActions.tsx new file mode 100644 index 000000000..b51328bb3 --- /dev/null +++ b/app/src/components/worktree/WorktreeActions.tsx @@ -0,0 +1,196 @@ +/** + * WorktreeActions — open / diff / remove affordances for one isolated worker + * `git worktree` (#3376). Shared between the inline subagent row (compact) in + * the conversation timeline and the Worktrees command-center panel (full). + * + * Safety contract (acceptance criterion of #3376): a **clean** worktree removes + * in one click; a **dirty** worktree requires an explicit user decision — the + * Remove button expands into a confirm row offering "Discard & remove" (force) + * or "Preserve" (cancel, keep the worktree + its `worker/*` branch). The core + * itself also refuses a dirty removal without `force`, so this is defense in + * depth, not the only guard. + */ +import debug from 'debug'; +import { useCallback, useState } from 'react'; + +import { useT } from '../../lib/i18n/I18nContext'; +import { worktreeApi } from '../../services/api/worktreeApi'; +import { revealPath } from '../../utils/openUrl'; + +const log = debug('worktree:actions'); + +export interface WorktreeActionsProps { + /** Absolute worktree checkout path. */ + path: string; + /** Whether the worktree has uncommitted changes. */ + isDirty?: boolean; + /** Compact styling for the inline subagent row (vs the full panel row). */ + compact?: boolean; + /** Called after a successful remove so the parent can refetch / drop the row. */ + onRemoved?: (path: string) => void; +} + +const btnBase = + 'rounded-md border px-2 py-0.5 font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50'; +const btnNeutral = + 'border-stone-200 text-stone-600 hover:bg-stone-50 dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-800'; +const btnDanger = + 'border-coral-200 text-coral-600 hover:bg-coral-50 dark:border-coral-500/40 dark:text-coral-300 dark:hover:bg-coral-500/10'; + +export default function WorktreeActions({ + path, + isDirty, + compact, + onRemoved, +}: WorktreeActionsProps) { + const { t } = useT(); + const [diffOpen, setDiffOpen] = useState(false); + const [diffText, setDiffText] = useState(null); + const [diffLoading, setDiffLoading] = useState(false); + const [confirmRemove, setConfirmRemove] = useState(false); + const [removing, setRemoving] = useState(false); + const [error, setError] = useState(null); + + const sizeText = compact ? 'text-[10px]' : 'text-xs'; + + const handleOpen = useCallback(() => { + log('open path=%s', path); + void revealPath(path).catch(err => { + log('open failed %O', err); + setError(err instanceof Error ? err.message : String(err)); + }); + }, [path]); + + const handleDiff = useCallback(async () => { + if (diffOpen) { + setDiffOpen(false); + return; + } + setDiffOpen(true); + setError(null); + // Fetch once; cache for subsequent toggles. + if (diffText !== null) return; + setDiffLoading(true); + try { + const summary = await worktreeApi.diff(path); + setDiffText(summary); + log('diff loaded path=%s chars=%d', path, summary.length); + } catch (err) { + log('diff failed %O', err); + setError(t('worktree.diffFailed')); + setDiffOpen(false); + } finally { + setDiffLoading(false); + } + }, [diffOpen, diffText, path, t]); + + const doRemove = useCallback( + async (force: boolean) => { + setRemoving(true); + setError(null); + try { + await worktreeApi.remove(path, force); + log('removed path=%s force=%s', path, force); + // The parent (e.g. the Worktrees panel) drops the row on success and + // this component unmounts. But inline in the chat timeline + // (`ToolTimelineBlock`) there is no `onRemoved`, so nothing unmounts + // us — reset local state here or the row stays stuck on "Removing…" + // with the button disabled forever even though the checkout is gone. + if (onRemoved) { + onRemoved(path); + } else { + setRemoving(false); + setConfirmRemove(false); + } + } catch (err) { + log('remove failed %O', err); + setError(t('worktree.removeFailed')); + setRemoving(false); + setConfirmRemove(false); + } + }, + [path, onRemoved, t] + ); + + const handleRemoveClick = useCallback(() => { + // Dirty worktrees must not be removed without an explicit choice. + if (isDirty) { + setConfirmRemove(true); + return; + } + void doRemove(false); + }, [isDirty, doRemove]); + + return ( +
+ {!confirmRemove ? ( +
+ + + +
+ ) : ( +
+

{t('worktree.dirtyConfirm')}

+
+ + +
+
+ )} + + {diffOpen ? ( +
+          {diffLoading
+            ? t('worktree.diffLoading')
+            : diffText && diffText.trim().length > 0
+              ? diffText
+              : t('worktree.diffEmpty')}
+        
+ ) : null} + + {error ? ( +

+ {error} +

+ ) : null} +
+ ); +} diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index bc5932102..9b71e5c23 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -409,6 +409,9 @@ const messages: TranslationMap = { 'memory.tab.agentWork': 'عمل الوكيل', 'memory.tab.agentWorkDescription': 'مركز قيادة لكل تشغيل وكيل في الخلفية — مُجمّع حسب ما يحتاج إلى مُدخلاتك، وما يعمل الآن، وما اكتمل.', + 'memory.tab.worktrees': 'أشجار العمل', + 'memory.tab.worktreesDescription': + 'أشجار عمل git معزولة من عُمّال الوكلاء المتوازين — افحص التغييرات، أو افتحها، أو نظّفها.', 'memory.tab.teams': 'الفِرَق', 'memory.tab.teamsDescription': 'فِرَق وكلاء منسَّقة — مَن يملك كل مهمة، وما المحظور، وما الذي يقوله الزملاء لبعضهم البعض.', @@ -2947,6 +2950,32 @@ const messages: TranslationMap = { 'intelligence.agentWork.kind.workflowChild': 'عنصر فرعي لسير العمل', 'intelligence.agentWork.openThread': 'فتح المحادثة', 'intelligence.agentWork.openWorker': 'فتح العامل', + 'worktree.label': 'شجرة العمل', + 'worktree.dirty': 'تغييرات غير مُلتزمة', + 'worktree.clean': 'نظيفة', + 'worktree.changedFile': 'ملف مُعدّل', + 'worktree.changedFiles': 'ملفات مُعدّلة', + 'worktree.removing': 'جارٍ الإزالة…', + 'worktree.removeFailed': 'فشل إزالة شجرة العمل', + 'worktree.diffFailed': 'فشل تحميل الفرق', + 'worktree.diffLoading': 'جارٍ تحميل الفرق…', + 'worktree.diffEmpty': 'لا توجد تغييرات مقارنةً بـ HEAD.', + 'worktree.dirtyConfirm': + 'تحتوي شجرة العمل هذه على تغييرات غير مُلتزمة. إزالتها تتجاهل ذلك العمل. يتم الاحتفاظ بفرع العامل.', + 'worktree.action.open': 'فتح المجلد', + 'worktree.action.diff': 'الفرق', + 'worktree.action.hideDiff': 'إخفاء الفرق', + 'worktree.action.remove': 'إزالة', + 'worktree.action.removeAnyway': 'تجاهل وإزالة', + 'worktree.action.preserve': 'الإبقاء', + 'worktree.panel.subtitle': 'أشجار عمل git معزولة أنشأها عُمّال الوكلاء المتوازون.', + 'worktree.panel.loading': 'جارٍ تحميل أشجار العمل…', + 'worktree.panel.failedToLoad': 'فشل تحميل أشجار العمل', + 'worktree.panel.empty': + 'لا توجد أشجار عمل معزولة. ستظهر هنا العُمّال المتوازون ذوو عزل شجرة العمل.', + 'worktree.panel.overlapsTitle': 'تغييرات متداخلة', + 'worktree.panel.overlapHint': + 'تم تعديل هذه الملفات بواسطة أكثر من شجرة عمل — وفّق بينها قبل الدمج.', 'intelligence.agentWork.action.stop': 'إيقاف', 'intelligence.agentWork.action.retry': 'إعادة المحاولة', 'intelligence.agentWork.action.continue': 'متابعة', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index c1e801430..a745ab5c2 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -415,6 +415,9 @@ const messages: TranslationMap = { 'memory.tab.agentWork': 'এজেন্ট কাজ', 'memory.tab.agentWorkDescription': 'প্রতিটি ব্যাকগ্রাউন্ড এজেন্ট রানের জন্য একটি কমান্ড সেন্টার — আপনার ইনপুট প্রয়োজন এমন, চলমান এবং সম্পন্ন অনুযায়ী গোষ্ঠীবদ্ধ।', + 'memory.tab.worktrees': 'ওয়ার্কট্রি', + 'memory.tab.worktreesDescription': + 'সমান্তরাল এজেন্ট ওয়ার্কারদের থেকে বিচ্ছিন্ন git ওয়ার্কট্রি — পরিবর্তন পরিদর্শন করুন, খুলুন বা পরিষ্কার করুন।', 'memory.tab.teams': 'টিম', 'memory.tab.teamsDescription': 'সমন্বিত এজেন্ট টিম — কে প্রতিটি কাজের মালিক, কী আটকে আছে, এবং সতীর্থরা একে অপরকে কী বলছে।', @@ -3009,6 +3012,32 @@ const messages: TranslationMap = { 'intelligence.agentWork.kind.workflowChild': 'ওয়ার্কফ্লো চাইল্ড', 'intelligence.agentWork.openThread': 'থ্রেড খুলুন', 'intelligence.agentWork.openWorker': 'ওয়ার্কার খুলুন', + 'worktree.label': 'ওয়ার্কট্রি', + 'worktree.dirty': 'কমিট না-করা পরিবর্তন', + 'worktree.clean': 'পরিচ্ছন্ন', + 'worktree.changedFile': 'পরিবর্তিত ফাইল', + 'worktree.changedFiles': 'পরিবর্তিত ফাইল', + 'worktree.removing': 'সরানো হচ্ছে…', + 'worktree.removeFailed': 'ওয়ার্কট্রি সরাতে ব্যর্থ', + 'worktree.diffFailed': 'ডিফ লোড করতে ব্যর্থ', + 'worktree.diffLoading': 'ডিফ লোড হচ্ছে…', + 'worktree.diffEmpty': 'HEAD-এর তুলনায় কোনো পরিবর্তন নেই।', + 'worktree.dirtyConfirm': + 'এই ওয়ার্কট্রিতে কমিট না-করা পরিবর্তন আছে। এটি সরালে সেই কাজ বাতিল হয়ে যাবে। ওয়ার্কার ব্রাঞ্চ রাখা হবে।', + 'worktree.action.open': 'ফোল্ডার খুলুন', + 'worktree.action.diff': 'ডিফ', + 'worktree.action.hideDiff': 'ডিফ লুকান', + 'worktree.action.remove': 'সরান', + 'worktree.action.removeAnyway': 'বাতিল করে সরান', + 'worktree.action.preserve': 'রেখে দিন', + 'worktree.panel.subtitle': 'সমান্তরাল এজেন্ট ওয়ার্কারদের তৈরি করা বিচ্ছিন্ন git ওয়ার্কট্রি।', + 'worktree.panel.loading': 'ওয়ার্কট্রি লোড হচ্ছে…', + 'worktree.panel.failedToLoad': 'ওয়ার্কট্রি লোড করতে ব্যর্থ', + 'worktree.panel.empty': + 'কোনো বিচ্ছিন্ন ওয়ার্কট্রি নেই। ওয়ার্কট্রি বিচ্ছিন্নকরণসহ সমান্তরাল ওয়ার্কাররা এখানে প্রদর্শিত হবে।', + 'worktree.panel.overlapsTitle': 'ওভারল্যাপিং পরিবর্তন', + 'worktree.panel.overlapHint': + 'এই ফাইলগুলো একাধিক ওয়ার্কট্রি দ্বারা পরিবর্তিত হয়েছে — মার্জ করার আগে সমন্বয় করুন।', 'intelligence.agentWork.action.stop': 'থামান', 'intelligence.agentWork.action.retry': 'আবার চেষ্টা করুন', 'intelligence.agentWork.action.continue': 'চালিয়ে যান', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index 988ee3f06..cba6bbabe 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -427,6 +427,9 @@ const messages: TranslationMap = { 'memory.tab.agentWork': 'Agentenarbeit', 'memory.tab.agentWorkDescription': 'Eine Kommandozentrale für jeden Hintergrund-Agentenlauf — gruppiert danach, was deine Eingabe braucht, was läuft und was abgeschlossen ist.', + 'memory.tab.worktrees': 'Worktrees', + 'memory.tab.worktreesDescription': + 'Isolierte Git-Worktrees von parallelen Agenten-Workern — Änderungen prüfen, öffnen oder aufräumen.', 'memory.tab.teams': 'Teams', 'memory.tab.teamsDescription': 'Koordinierte Agententeams — wer für welche Aufgabe zuständig ist, was blockiert ist und was sich die Teammitglieder gegenseitig mitteilen.', @@ -3081,6 +3084,33 @@ const messages: TranslationMap = { 'intelligence.agentWork.kind.workflowChild': 'Workflow-Unterelement', 'intelligence.agentWork.openThread': 'Thread öffnen', 'intelligence.agentWork.openWorker': 'Worker öffnen', + 'worktree.label': 'Worktree', + 'worktree.dirty': 'Nicht committete Änderungen', + 'worktree.clean': 'Sauber', + 'worktree.changedFile': 'geänderte Datei', + 'worktree.changedFiles': 'geänderte Dateien', + 'worktree.removing': 'Wird entfernt…', + 'worktree.removeFailed': 'Worktree konnte nicht entfernt werden', + 'worktree.diffFailed': 'Diff konnte nicht geladen werden', + 'worktree.diffLoading': 'Diff wird geladen…', + 'worktree.diffEmpty': 'Keine Änderungen gegenüber HEAD.', + 'worktree.dirtyConfirm': + 'Dieser Worktree hat nicht committete Änderungen. Beim Entfernen geht diese Arbeit verloren. Der Worker-Branch bleibt erhalten.', + 'worktree.action.open': 'Ordner öffnen', + 'worktree.action.diff': 'Diff', + 'worktree.action.hideDiff': 'Diff ausblenden', + 'worktree.action.remove': 'Entfernen', + 'worktree.action.removeAnyway': 'Verwerfen & entfernen', + 'worktree.action.preserve': 'Beibehalten', + 'worktree.panel.subtitle': + 'Isolierte Git-Worktrees, die von parallelen Agenten-Workern erstellt wurden.', + 'worktree.panel.loading': 'Worktrees werden geladen…', + 'worktree.panel.failedToLoad': 'Worktrees konnten nicht geladen werden', + 'worktree.panel.empty': + 'Keine isolierten Worktrees. Parallele Worker mit Worktree-Isolation erscheinen hier.', + 'worktree.panel.overlapsTitle': 'Überlappende Änderungen', + 'worktree.panel.overlapHint': + 'Diese Dateien wurden von mehr als einem Worktree geändert — vor dem Mergen abgleichen.', 'intelligence.agentWork.action.stop': 'Stoppen', 'intelligence.agentWork.action.retry': 'Wiederholen', 'intelligence.agentWork.action.continue': 'Fortsetzen', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index 442581b28..d90c199e0 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -455,6 +455,9 @@ const en: TranslationMap = { 'memory.tab.agentWork': 'Agent Work', 'memory.tab.agentWorkDescription': 'A command center for every background agent run — grouped by what needs your input, what is working, and what has finished.', + 'memory.tab.worktrees': 'Worktrees', + 'memory.tab.worktreesDescription': + 'Isolated git worktrees from parallel agent workers — inspect changes, open them, or clean them up.', 'memory.tab.teams': 'Teams', 'memory.tab.teamsDescription': 'Coordinated agent teams — who owns each task, what is blocked, and what the teammates are saying to each other.', @@ -3551,6 +3554,33 @@ const en: TranslationMap = { 'intelligence.agentWork.kind.workflowChild': 'Workflow child', 'intelligence.agentWork.openThread': 'Open thread', 'intelligence.agentWork.openWorker': 'Open worker', + // Worktree manager (#3376) — isolated worker git worktrees + 'worktree.label': 'Worktree', + 'worktree.dirty': 'Uncommitted changes', + 'worktree.clean': 'Clean', + 'worktree.changedFile': 'changed file', + 'worktree.changedFiles': 'changed files', + 'worktree.removing': 'Removing…', + 'worktree.removeFailed': 'Failed to remove worktree', + 'worktree.diffFailed': 'Failed to load diff', + 'worktree.diffLoading': 'Loading diff…', + 'worktree.diffEmpty': 'No changes vs HEAD.', + 'worktree.dirtyConfirm': + 'This worktree has uncommitted changes. Removing it discards that work. The worker branch is kept.', + 'worktree.action.open': 'Open folder', + 'worktree.action.diff': 'Diff', + 'worktree.action.hideDiff': 'Hide diff', + 'worktree.action.remove': 'Remove', + 'worktree.action.removeAnyway': 'Discard & remove', + 'worktree.action.preserve': 'Preserve', + 'worktree.panel.subtitle': 'Isolated git worktrees created by parallel agent workers.', + 'worktree.panel.loading': 'Loading worktrees…', + 'worktree.panel.failedToLoad': 'Failed to load worktrees', + 'worktree.panel.empty': + 'No isolated worktrees. Parallel workers with worktree isolation will appear here.', + 'worktree.panel.overlapsTitle': 'Overlapping changes', + 'worktree.panel.overlapHint': + 'These files were changed by more than one worktree — reconcile before merging.', 'intelligence.agentWork.action.stop': 'Stop', 'intelligence.agentWork.action.retry': 'Retry', 'intelligence.agentWork.action.continue': 'Continue', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index 9509a1aa9..7958e6ce1 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -427,6 +427,9 @@ const messages: TranslationMap = { 'memory.tab.agentWork': 'Trabajo del agente', 'memory.tab.agentWorkDescription': 'Un centro de mando para cada ejecución de agente en segundo plano, agrupado según lo que necesita tu intervención, lo que está en curso y lo que ha terminado.', + 'memory.tab.worktrees': 'Worktrees', + 'memory.tab.worktreesDescription': + 'Worktrees de git aislados de los workers de agentes en paralelo: inspecciona los cambios, ábrelos o límpialos.', 'memory.tab.teams': 'Equipos', 'memory.tab.teamsDescription': 'Equipos de agentes coordinados: quién se encarga de cada tarea, qué está bloqueado y qué se dicen los compañeros entre sí.', @@ -3064,6 +3067,33 @@ const messages: TranslationMap = { 'intelligence.agentWork.kind.workflowChild': 'Hijo de flujo de trabajo', 'intelligence.agentWork.openThread': 'Abrir hilo', 'intelligence.agentWork.openWorker': 'Abrir trabajador', + 'worktree.label': 'Worktree', + 'worktree.dirty': 'Cambios sin confirmar', + 'worktree.clean': 'Limpio', + 'worktree.changedFile': 'archivo modificado', + 'worktree.changedFiles': 'archivos modificados', + 'worktree.removing': 'Eliminando…', + 'worktree.removeFailed': 'No se pudo eliminar el worktree', + 'worktree.diffFailed': 'No se pudo cargar el diff', + 'worktree.diffLoading': 'Cargando diff…', + 'worktree.diffEmpty': 'Sin cambios respecto a HEAD.', + 'worktree.dirtyConfirm': + 'Este worktree tiene cambios sin confirmar. Eliminarlo descarta ese trabajo. La rama del worker se conserva.', + 'worktree.action.open': 'Abrir carpeta', + 'worktree.action.diff': 'Diff', + 'worktree.action.hideDiff': 'Ocultar diff', + 'worktree.action.remove': 'Eliminar', + 'worktree.action.removeAnyway': 'Descartar y eliminar', + 'worktree.action.preserve': 'Conservar', + 'worktree.panel.subtitle': + 'Worktrees de git aislados creados por workers de agentes en paralelo.', + 'worktree.panel.loading': 'Cargando worktrees…', + 'worktree.panel.failedToLoad': 'No se pudieron cargar los worktrees', + 'worktree.panel.empty': + 'No hay worktrees aislados. Los workers en paralelo con aislamiento de worktree aparecerán aquí.', + 'worktree.panel.overlapsTitle': 'Cambios superpuestos', + 'worktree.panel.overlapHint': + 'Estos archivos fueron modificados por más de un worktree: concilia antes de fusionar.', 'intelligence.agentWork.action.stop': 'Detener', 'intelligence.agentWork.action.retry': 'Reintentar', 'intelligence.agentWork.action.continue': 'Continuar', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index bc48a1f94..5ecdfb5c2 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -428,6 +428,9 @@ const messages: TranslationMap = { 'memory.tab.agentWork': "Travail de l'agent", 'memory.tab.agentWorkDescription': "Un centre de commande pour chaque exécution d'agent en arrière-plan, regroupée selon ce qui nécessite votre intervention, ce qui est en cours et ce qui est terminé.", + 'memory.tab.worktrees': 'Worktrees', + 'memory.tab.worktreesDescription': + 'Worktrees git isolés des workers d’agents parallèles — inspectez les modifications, ouvrez-les ou nettoyez-les.', 'memory.tab.teams': 'Équipes', 'memory.tab.teamsDescription': "Équipes d'agents coordonnées — qui s'occupe de chaque tâche, ce qui est bloqué et ce que les coéquipiers se disent entre eux.", @@ -3080,6 +3083,32 @@ const messages: TranslationMap = { 'intelligence.agentWork.kind.workflowChild': 'Élément enfant de workflow', 'intelligence.agentWork.openThread': 'Ouvrir le fil', 'intelligence.agentWork.openWorker': 'Ouvrir le worker', + 'worktree.label': 'Worktree', + 'worktree.dirty': 'Modifications non validées', + 'worktree.clean': 'Propre', + 'worktree.changedFile': 'fichier modifié', + 'worktree.changedFiles': 'fichiers modifiés', + 'worktree.removing': 'Suppression…', + 'worktree.removeFailed': 'Échec de la suppression du worktree', + 'worktree.diffFailed': 'Échec du chargement du diff', + 'worktree.diffLoading': 'Chargement du diff…', + 'worktree.diffEmpty': 'Aucune modification par rapport à HEAD.', + 'worktree.dirtyConfirm': + 'Ce worktree contient des modifications non validées. Le supprimer abandonne ce travail. La branche du worker est conservée.', + 'worktree.action.open': 'Ouvrir le dossier', + 'worktree.action.diff': 'Diff', + 'worktree.action.hideDiff': 'Masquer le diff', + 'worktree.action.remove': 'Supprimer', + 'worktree.action.removeAnyway': 'Abandonner et supprimer', + 'worktree.action.preserve': 'Conserver', + 'worktree.panel.subtitle': 'Worktrees git isolés créés par des workers d’agents parallèles.', + 'worktree.panel.loading': 'Chargement des worktrees…', + 'worktree.panel.failedToLoad': 'Échec du chargement des worktrees', + 'worktree.panel.empty': + 'Aucun worktree isolé. Les workers parallèles avec isolation par worktree apparaîtront ici.', + 'worktree.panel.overlapsTitle': 'Modifications qui se chevauchent', + 'worktree.panel.overlapHint': + 'Ces fichiers ont été modifiés par plusieurs worktrees — réconciliez-les avant de fusionner.', 'intelligence.agentWork.action.stop': 'Arrêter', 'intelligence.agentWork.action.retry': 'Réessayer', 'intelligence.agentWork.action.continue': 'Continuer', diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index da3fed25a..65473aa9a 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -413,6 +413,9 @@ const messages: TranslationMap = { 'memory.tab.agentWork': 'एजेंट कार्य', 'memory.tab.agentWorkDescription': 'हर पृष्ठभूमि एजेंट रन के लिए एक कमांड सेंटर — इस आधार पर समूहित कि किसे आपकी जानकारी चाहिए, क्या चल रहा है और क्या पूरा हो चुका है।', + 'memory.tab.worktrees': 'वर्कट्री', + 'memory.tab.worktreesDescription': + 'समानांतर एजेंट वर्कर्स से पृथक git वर्कट्री — बदलावों का निरीक्षण करें, उन्हें खोलें या साफ़ करें।', 'memory.tab.teams': 'टीमें', 'memory.tab.teamsDescription': 'समन्वित एजेंट टीमें — हर कार्य का स्वामी कौन है, क्या अवरुद्ध है, और सहयोगी एक-दूसरे से क्या कह रहे हैं।', @@ -3010,6 +3013,32 @@ const messages: TranslationMap = { 'intelligence.agentWork.kind.workflowChild': 'वर्कफ़्लो चाइल्ड', 'intelligence.agentWork.openThread': 'थ्रेड खोलें', 'intelligence.agentWork.openWorker': 'वर्कर खोलें', + 'worktree.label': 'वर्कट्री', + 'worktree.dirty': 'बिना कमिट किए बदलाव', + 'worktree.clean': 'साफ़', + 'worktree.changedFile': 'बदली गई फ़ाइल', + 'worktree.changedFiles': 'बदली गई फ़ाइलें', + 'worktree.removing': 'हटाया जा रहा है…', + 'worktree.removeFailed': 'वर्कट्री हटाने में विफल', + 'worktree.diffFailed': 'डिफ़ लोड करने में विफल', + 'worktree.diffLoading': 'डिफ़ लोड हो रहा है…', + 'worktree.diffEmpty': 'HEAD की तुलना में कोई बदलाव नहीं।', + 'worktree.dirtyConfirm': + 'इस वर्कट्री में बिना कमिट किए बदलाव हैं। इसे हटाने पर वह काम छूट जाएगा। वर्कर ब्रांच बनी रहती है।', + 'worktree.action.open': 'फ़ोल्डर खोलें', + 'worktree.action.diff': 'डिफ़', + 'worktree.action.hideDiff': 'डिफ़ छिपाएँ', + 'worktree.action.remove': 'हटाएँ', + 'worktree.action.removeAnyway': 'छोड़ें और हटाएँ', + 'worktree.action.preserve': 'सुरक्षित रखें', + 'worktree.panel.subtitle': 'समानांतर एजेंट वर्कर्स द्वारा बनाए गए पृथक git वर्कट्री।', + 'worktree.panel.loading': 'वर्कट्री लोड हो रही हैं…', + 'worktree.panel.failedToLoad': 'वर्कट्री लोड करने में विफल', + 'worktree.panel.empty': + 'कोई पृथक वर्कट्री नहीं। वर्कट्री पृथक्करण वाले समानांतर वर्कर्स यहाँ दिखाई देंगे।', + 'worktree.panel.overlapsTitle': 'ओवरलैपिंग बदलाव', + 'worktree.panel.overlapHint': + 'ये फ़ाइलें एक से अधिक वर्कट्री द्वारा बदली गई थीं — मर्ज करने से पहले समाधान करें।', 'intelligence.agentWork.action.stop': 'रोकें', 'intelligence.agentWork.action.retry': 'फिर से प्रयास करें', 'intelligence.agentWork.action.continue': 'जारी रखें', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index 41f23c58c..21c5d8aec 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -417,6 +417,9 @@ const messages: TranslationMap = { 'memory.tab.agentWork': 'Kerja agen', 'memory.tab.agentWorkDescription': 'Pusat kendali untuk setiap proses agen latar belakang — dikelompokkan berdasarkan apa yang butuh masukan Anda, apa yang sedang berjalan, dan apa yang sudah selesai.', + 'memory.tab.worktrees': 'Worktree', + 'memory.tab.worktreesDescription': + 'Worktree git terisolasi dari worker agen paralel — periksa perubahan, buka, atau bersihkan.', 'memory.tab.teams': 'Tim', 'memory.tab.teamsDescription': 'Tim agen yang terkoordinasi — siapa yang menangani setiap tugas, apa yang terhambat, dan apa yang dibicarakan rekan satu tim satu sama lain.', @@ -3014,6 +3017,32 @@ const messages: TranslationMap = { 'intelligence.agentWork.kind.workflowChild': 'Anak alur kerja', 'intelligence.agentWork.openThread': 'Buka utas', 'intelligence.agentWork.openWorker': 'Buka pekerja', + 'worktree.label': 'Worktree', + 'worktree.dirty': 'Perubahan belum di-commit', + 'worktree.clean': 'Bersih', + 'worktree.changedFile': 'berkas berubah', + 'worktree.changedFiles': 'berkas berubah', + 'worktree.removing': 'Menghapus…', + 'worktree.removeFailed': 'Gagal menghapus worktree', + 'worktree.diffFailed': 'Gagal memuat diff', + 'worktree.diffLoading': 'Memuat diff…', + 'worktree.diffEmpty': 'Tidak ada perubahan dibandingkan HEAD.', + 'worktree.dirtyConfirm': + 'Worktree ini memiliki perubahan yang belum di-commit. Menghapusnya akan membuang pekerjaan itu. Branch worker tetap dipertahankan.', + 'worktree.action.open': 'Buka folder', + 'worktree.action.diff': 'Diff', + 'worktree.action.hideDiff': 'Sembunyikan diff', + 'worktree.action.remove': 'Hapus', + 'worktree.action.removeAnyway': 'Buang & hapus', + 'worktree.action.preserve': 'Pertahankan', + 'worktree.panel.subtitle': 'Worktree git terisolasi yang dibuat oleh worker agen paralel.', + 'worktree.panel.loading': 'Memuat worktree…', + 'worktree.panel.failedToLoad': 'Gagal memuat worktree', + 'worktree.panel.empty': + 'Tidak ada worktree terisolasi. Worker paralel dengan isolasi worktree akan muncul di sini.', + 'worktree.panel.overlapsTitle': 'Perubahan yang tumpang tindih', + 'worktree.panel.overlapHint': + 'Berkas-berkas ini diubah oleh lebih dari satu worktree — selaraskan sebelum menggabungkan.', 'intelligence.agentWork.action.stop': 'Hentikan', 'intelligence.agentWork.action.retry': 'Coba lagi', 'intelligence.agentWork.action.continue': 'Lanjutkan', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index eb0c2d0a1..c85557062 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -424,6 +424,9 @@ const messages: TranslationMap = { 'memory.tab.agentWork': "Lavoro dell'agente", 'memory.tab.agentWorkDescription': 'Un centro di comando per ogni esecuzione di agente in background, raggruppata in base a ciò che richiede il tuo intervento, ciò che è in corso e ciò che è terminato.', + 'memory.tab.worktrees': 'Worktree', + 'memory.tab.worktreesDescription': + 'Worktree git isolati dai worker degli agenti paralleli: ispeziona le modifiche, aprile o ripuliscile.', 'memory.tab.teams': 'Team', 'memory.tab.teamsDescription': 'Team di agenti coordinati — chi è responsabile di ogni attività, cosa è bloccato e cosa si dicono i compagni di team tra loro.', @@ -3057,6 +3060,32 @@ const messages: TranslationMap = { 'intelligence.agentWork.kind.workflowChild': 'Figlio del flusso di lavoro', 'intelligence.agentWork.openThread': 'Apri thread', 'intelligence.agentWork.openWorker': 'Apri worker', + 'worktree.label': 'Worktree', + 'worktree.dirty': 'Modifiche non sottoposte a commit', + 'worktree.clean': 'Pulito', + 'worktree.changedFile': 'file modificato', + 'worktree.changedFiles': 'file modificati', + 'worktree.removing': 'Rimozione…', + 'worktree.removeFailed': 'Impossibile rimuovere il worktree', + 'worktree.diffFailed': 'Impossibile caricare il diff', + 'worktree.diffLoading': 'Caricamento del diff…', + 'worktree.diffEmpty': 'Nessuna modifica rispetto a HEAD.', + 'worktree.dirtyConfirm': + 'Questo worktree ha modifiche non sottoposte a commit. Rimuoverlo scarta quel lavoro. Il branch del worker viene mantenuto.', + 'worktree.action.open': 'Apri cartella', + 'worktree.action.diff': 'Diff', + 'worktree.action.hideDiff': 'Nascondi diff', + 'worktree.action.remove': 'Rimuovi', + 'worktree.action.removeAnyway': 'Scarta e rimuovi', + 'worktree.action.preserve': 'Conserva', + 'worktree.panel.subtitle': 'Worktree git isolati creati da worker di agenti paralleli.', + 'worktree.panel.loading': 'Caricamento dei worktree…', + 'worktree.panel.failedToLoad': 'Impossibile caricare i worktree', + 'worktree.panel.empty': + 'Nessun worktree isolato. I worker paralleli con isolamento tramite worktree appariranno qui.', + 'worktree.panel.overlapsTitle': 'Modifiche sovrapposte', + 'worktree.panel.overlapHint': + 'Questi file sono stati modificati da più di un worktree: riconcilia prima di unire.', 'intelligence.agentWork.action.stop': 'Interrompi', 'intelligence.agentWork.action.retry': 'Riprova', 'intelligence.agentWork.action.continue': 'Continua', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index 036edefe7..28152a9fe 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -414,6 +414,9 @@ const messages: TranslationMap = { 'memory.tab.agentWork': '에이전트 작업', 'memory.tab.agentWorkDescription': '모든 백그라운드 에이전트 실행을 위한 명령 센터 — 입력이 필요한 것, 진행 중인 것, 완료된 것별로 그룹화됩니다.', + 'memory.tab.worktrees': '워크트리', + 'memory.tab.worktreesDescription': + '병렬 에이전트 워커가 만든 격리된 git 워크트리 — 변경 사항을 확인하거나 열거나 정리하세요.', 'memory.tab.teams': '팀', 'memory.tab.teamsDescription': '조율된 에이전트 팀 — 각 작업을 누가 담당하는지, 무엇이 차단되어 있는지, 그리고 팀원들이 서로 무슨 이야기를 나누는지.', @@ -2985,6 +2988,32 @@ const messages: TranslationMap = { 'intelligence.agentWork.kind.workflowChild': '워크플로 하위 항목', 'intelligence.agentWork.openThread': '스레드 열기', 'intelligence.agentWork.openWorker': '워커 열기', + 'worktree.label': '워크트리', + 'worktree.dirty': '커밋되지 않은 변경 사항', + 'worktree.clean': '깨끗함', + 'worktree.changedFile': '변경된 파일', + 'worktree.changedFiles': '변경된 파일', + 'worktree.removing': '제거 중…', + 'worktree.removeFailed': '워크트리 제거 실패', + 'worktree.diffFailed': 'diff 불러오기 실패', + 'worktree.diffLoading': 'diff 불러오는 중…', + 'worktree.diffEmpty': 'HEAD 대비 변경 사항이 없습니다.', + 'worktree.dirtyConfirm': + '이 워크트리에는 커밋되지 않은 변경 사항이 있습니다. 제거하면 해당 작업이 삭제됩니다. 워커 브랜치는 유지됩니다.', + 'worktree.action.open': '폴더 열기', + 'worktree.action.diff': 'Diff', + 'worktree.action.hideDiff': 'diff 숨기기', + 'worktree.action.remove': '제거', + 'worktree.action.removeAnyway': '버리고 제거', + 'worktree.action.preserve': '보존', + 'worktree.panel.subtitle': '병렬 에이전트 워커가 생성한 격리된 git 워크트리입니다.', + 'worktree.panel.loading': '워크트리 불러오는 중…', + 'worktree.panel.failedToLoad': '워크트리 불러오기 실패', + 'worktree.panel.empty': + '격리된 워크트리가 없습니다. 워크트리 격리를 사용하는 병렬 워커가 여기에 표시됩니다.', + 'worktree.panel.overlapsTitle': '겹치는 변경 사항', + 'worktree.panel.overlapHint': + '이 파일들은 둘 이상의 워크트리에서 변경되었습니다 — 병합하기 전에 조정하세요.', 'intelligence.agentWork.action.stop': '중지', 'intelligence.agentWork.action.retry': '다시 시도', 'intelligence.agentWork.action.continue': '계속', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index 7c393b95e..308171e53 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -420,6 +420,9 @@ const messages: TranslationMap = { 'memory.tab.agentWork': 'Praca agenta', 'memory.tab.agentWorkDescription': 'Centrum dowodzenia dla każdego przebiegu agenta w tle — pogrupowane według tego, co wymaga Twojej reakcji, co działa i co zostało ukończone.', + 'memory.tab.worktrees': 'Worktree', + 'memory.tab.worktreesDescription': + 'Izolowane drzewa robocze git od równoległych procesów agentów — przejrzyj zmiany, otwórz je lub wyczyść.', 'memory.tab.teams': 'Zespoły', 'memory.tab.teamsDescription': 'Skoordynowane zespoły agentów — kto odpowiada za każde zadanie, co jest zablokowane i co członkowie zespołu mówią sobie nawzajem.', @@ -3047,6 +3050,33 @@ const messages: TranslationMap = { 'intelligence.agentWork.kind.workflowChild': 'Element podrzędny przepływu', 'intelligence.agentWork.openThread': 'Otwórz wątek', 'intelligence.agentWork.openWorker': 'Otwórz proces', + 'worktree.label': 'Worktree', + 'worktree.dirty': 'Niezatwierdzone zmiany', + 'worktree.clean': 'Czyste', + 'worktree.changedFile': 'zmieniony plik', + 'worktree.changedFiles': 'zmienione pliki', + 'worktree.removing': 'Usuwanie…', + 'worktree.removeFailed': 'Nie udało się usunąć worktree', + 'worktree.diffFailed': 'Nie udało się załadować diffa', + 'worktree.diffLoading': 'Ładowanie diffa…', + 'worktree.diffEmpty': 'Brak zmian względem HEAD.', + 'worktree.dirtyConfirm': + 'To worktree ma niezatwierdzone zmiany. Jego usunięcie odrzuci tę pracę. Gałąź workera zostaje zachowana.', + 'worktree.action.open': 'Otwórz folder', + 'worktree.action.diff': 'Diff', + 'worktree.action.hideDiff': 'Ukryj diff', + 'worktree.action.remove': 'Usuń', + 'worktree.action.removeAnyway': 'Odrzuć i usuń', + 'worktree.action.preserve': 'Zachowaj', + 'worktree.panel.subtitle': + 'Izolowane drzewa robocze git utworzone przez równoległe procesy agentów.', + 'worktree.panel.loading': 'Ładowanie worktree…', + 'worktree.panel.failedToLoad': 'Nie udało się załadować worktree', + 'worktree.panel.empty': + 'Brak izolowanych worktree. Pojawią się tutaj równoległe procesy z izolacją worktree.', + 'worktree.panel.overlapsTitle': 'Nakładające się zmiany', + 'worktree.panel.overlapHint': + 'Te pliki zostały zmienione przez więcej niż jedno worktree — uzgodnij je przed scaleniem.', 'intelligence.agentWork.action.stop': 'Zatrzymaj', 'intelligence.agentWork.action.retry': 'Ponów', 'intelligence.agentWork.action.continue': 'Kontynuuj', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index 40e4328b0..534b51749 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -427,6 +427,9 @@ const messages: TranslationMap = { 'memory.tab.agentWork': 'Trabalho do agente', 'memory.tab.agentWorkDescription': 'Um centro de comando para cada execução de agente em segundo plano, agrupada conforme o que precisa da sua intervenção, o que está em andamento e o que foi concluído.', + 'memory.tab.worktrees': 'Worktrees', + 'memory.tab.worktreesDescription': + 'Worktrees git isolados dos workers de agentes paralelos — inspecione as alterações, abra-as ou limpe-as.', 'memory.tab.teams': 'Equipes', 'memory.tab.teamsDescription': 'Equipes de agentes coordenadas — quem é responsável por cada tarefa, o que está bloqueado e o que os colegas de equipe estão dizendo uns aos outros.', @@ -3061,6 +3064,32 @@ const messages: TranslationMap = { 'intelligence.agentWork.kind.workflowChild': 'Filho de fluxo de trabalho', 'intelligence.agentWork.openThread': 'Abrir tópico', 'intelligence.agentWork.openWorker': 'Abrir trabalhador', + 'worktree.label': 'Worktree', + 'worktree.dirty': 'Alterações não confirmadas', + 'worktree.clean': 'Limpo', + 'worktree.changedFile': 'arquivo alterado', + 'worktree.changedFiles': 'arquivos alterados', + 'worktree.removing': 'Removendo…', + 'worktree.removeFailed': 'Falha ao remover o worktree', + 'worktree.diffFailed': 'Falha ao carregar o diff', + 'worktree.diffLoading': 'Carregando diff…', + 'worktree.diffEmpty': 'Nenhuma alteração em relação ao HEAD.', + 'worktree.dirtyConfirm': + 'Este worktree tem alterações não confirmadas. Removê-lo descarta esse trabalho. O branch do worker é mantido.', + 'worktree.action.open': 'Abrir pasta', + 'worktree.action.diff': 'Diff', + 'worktree.action.hideDiff': 'Ocultar diff', + 'worktree.action.remove': 'Remover', + 'worktree.action.removeAnyway': 'Descartar e remover', + 'worktree.action.preserve': 'Preservar', + 'worktree.panel.subtitle': 'Worktrees git isolados criados por workers de agentes paralelos.', + 'worktree.panel.loading': 'Carregando worktrees…', + 'worktree.panel.failedToLoad': 'Falha ao carregar os worktrees', + 'worktree.panel.empty': + 'Nenhum worktree isolado. Workers paralelos com isolamento por worktree aparecerão aqui.', + 'worktree.panel.overlapsTitle': 'Alterações sobrepostas', + 'worktree.panel.overlapHint': + 'Estes arquivos foram alterados por mais de um worktree — reconcilie antes de mesclar.', 'intelligence.agentWork.action.stop': 'Parar', 'intelligence.agentWork.action.retry': 'Tentar novamente', 'intelligence.agentWork.action.continue': 'Continuar', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index e982ccca7..3d96682c2 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -419,6 +419,9 @@ const messages: TranslationMap = { 'memory.tab.agentWork': 'Работа агента', 'memory.tab.agentWorkDescription': 'Центр управления для каждого фонового запуска агента — сгруппировано по тому, что требует вашего участия, что выполняется и что завершено.', + 'memory.tab.worktrees': 'Worktree', + 'memory.tab.worktreesDescription': + 'Изолированные рабочие деревья git от параллельных воркеров-агентов — просматривайте изменения, открывайте их или удаляйте.', 'memory.tab.teams': 'Команды', 'memory.tab.teamsDescription': 'Скоординированные команды агентов — кто отвечает за каждую задачу, что заблокировано и что участники говорят друг другу.', @@ -3033,6 +3036,33 @@ const messages: TranslationMap = { 'intelligence.agentWork.kind.workflowChild': 'Дочерний элемент рабочего процесса', 'intelligence.agentWork.openThread': 'Открыть тред', 'intelligence.agentWork.openWorker': 'Открыть воркер', + 'worktree.label': 'Worktree', + 'worktree.dirty': 'Незакоммиченные изменения', + 'worktree.clean': 'Чисто', + 'worktree.changedFile': 'изменённый файл', + 'worktree.changedFiles': 'изменённых файлов', + 'worktree.removing': 'Удаление…', + 'worktree.removeFailed': 'Не удалось удалить worktree', + 'worktree.diffFailed': 'Не удалось загрузить diff', + 'worktree.diffLoading': 'Загрузка diff…', + 'worktree.diffEmpty': 'Нет изменений относительно HEAD.', + 'worktree.dirtyConfirm': + 'В этом worktree есть незакоммиченные изменения. При удалении эта работа будет потеряна. Ветка воркера сохраняется.', + 'worktree.action.open': 'Открыть папку', + 'worktree.action.diff': 'Diff', + 'worktree.action.hideDiff': 'Скрыть diff', + 'worktree.action.remove': 'Удалить', + 'worktree.action.removeAnyway': 'Отклонить и удалить', + 'worktree.action.preserve': 'Сохранить', + 'worktree.panel.subtitle': + 'Изолированные рабочие деревья git, созданные параллельными воркерами-агентами.', + 'worktree.panel.loading': 'Загрузка worktree…', + 'worktree.panel.failedToLoad': 'Не удалось загрузить worktree', + 'worktree.panel.empty': + 'Нет изолированных worktree. Здесь появятся параллельные воркеры с изоляцией worktree.', + 'worktree.panel.overlapsTitle': 'Пересекающиеся изменения', + 'worktree.panel.overlapHint': + 'Эти файлы были изменены более чем одним worktree — согласуйте их перед слиянием.', 'intelligence.agentWork.action.stop': 'Остановить', 'intelligence.agentWork.action.retry': 'Повторить', 'intelligence.agentWork.action.continue': 'Продолжить', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index 5a243f380..ec4d2ef78 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -397,6 +397,9 @@ const messages: TranslationMap = { 'memory.tab.agentWork': '智能体工作', 'memory.tab.agentWorkDescription': '所有后台智能体运行的指挥中心——按需要你处理的、正在运行的和已完成的进行分组。', + 'memory.tab.worktrees': '工作树', + 'memory.tab.worktreesDescription': + '来自并行智能体工作进程的隔离 git 工作树——检查更改、打开它们或清理它们。', 'memory.tab.teams': '团队', 'memory.tab.teamsDescription': '协同的智能体团队——谁负责每项任务、有什么被阻塞,以及队友之间在交流什么。', @@ -2865,6 +2868,29 @@ const messages: TranslationMap = { 'intelligence.agentWork.kind.workflowChild': '工作流子项', 'intelligence.agentWork.openThread': '打开会话', 'intelligence.agentWork.openWorker': '打开工作线程', + 'worktree.label': '工作树', + 'worktree.dirty': '未提交的更改', + 'worktree.clean': '干净', + 'worktree.changedFile': '个更改的文件', + 'worktree.changedFiles': '个更改的文件', + 'worktree.removing': '正在移除…', + 'worktree.removeFailed': '移除工作树失败', + 'worktree.diffFailed': '加载差异失败', + 'worktree.diffLoading': '正在加载差异…', + 'worktree.diffEmpty': '与 HEAD 相比没有更改。', + 'worktree.dirtyConfirm': '此工作树有未提交的更改。移除它将丢弃这些工作。工作进程分支会保留。', + 'worktree.action.open': '打开文件夹', + 'worktree.action.diff': '差异', + 'worktree.action.hideDiff': '隐藏差异', + 'worktree.action.remove': '移除', + 'worktree.action.removeAnyway': '丢弃并移除', + 'worktree.action.preserve': '保留', + 'worktree.panel.subtitle': '由并行智能体工作进程创建的隔离 git 工作树。', + 'worktree.panel.loading': '正在加载工作树…', + 'worktree.panel.failedToLoad': '加载工作树失败', + 'worktree.panel.empty': '没有隔离的工作树。具有工作树隔离的并行工作进程将显示在此处。', + 'worktree.panel.overlapsTitle': '重叠的更改', + 'worktree.panel.overlapHint': '这些文件被多个工作树更改过——合并前请先协调。', 'intelligence.agentWork.action.stop': '停止', 'intelligence.agentWork.action.retry': '重试', 'intelligence.agentWork.action.continue': '继续', diff --git a/app/src/pages/Intelligence.tsx b/app/src/pages/Intelligence.tsx index ca8508d9e..0993af1f8 100644 --- a/app/src/pages/Intelligence.tsx +++ b/app/src/pages/Intelligence.tsx @@ -9,6 +9,7 @@ import IntelligenceOrchestrationTab from '../components/intelligence/Intelligenc import IntelligenceSubconsciousTab from '../components/intelligence/IntelligenceSubconsciousTab'; import IntelligenceTasksTab from '../components/intelligence/IntelligenceTasksTab'; import IntelligenceTeamsTab from '../components/intelligence/IntelligenceTeamsTab'; +import IntelligenceWorktreesTab from '../components/intelligence/IntelligenceWorktreesTab'; import MemorySection from '../components/intelligence/MemorySection'; import ModelCouncilTab from '../components/intelligence/ModelCouncilTab'; import { ToastContainer } from '../components/intelligence/Toast'; @@ -35,6 +36,7 @@ type IntelligenceTab = | 'subconscious' | 'tasks' | 'agent-work' + | 'worktrees' | 'teams' | 'agents' | 'workflows' @@ -46,6 +48,7 @@ const INTELLIGENCE_TABS: IntelligenceTab[] = [ 'subconscious', 'tasks', 'agent-work', + 'worktrees', 'teams', 'agents', 'workflows', @@ -173,6 +176,11 @@ export default function Intelligence({ tabParamKey = 'tab' }: IntelligenceProps label: t('memory.tab.agentWork'), description: t('memory.tab.agentWorkDescription'), }, + { + id: 'worktrees', + label: t('memory.tab.worktrees'), + description: t('memory.tab.worktreesDescription'), + }, { id: 'teams', label: t('memory.tab.teams'), description: t('memory.tab.teamsDescription') }, { id: 'memory', label: t('memory.tab.memory') }, { id: 'subconscious', label: t('memory.tab.subconscious') }, @@ -275,6 +283,8 @@ export default function Intelligence({ tabParamKey = 'tab' }: IntelligenceProps {activeTab === 'agent-work' && } + {activeTab === 'worktrees' && } + {activeTab === 'teams' && } {activeTab === 'agents' && } diff --git a/app/src/pages/conversations/components/ToolTimelineBlock.tsx b/app/src/pages/conversations/components/ToolTimelineBlock.tsx index 01703ae6b..d1b774737 100644 --- a/app/src/pages/conversations/components/ToolTimelineBlock.tsx +++ b/app/src/pages/conversations/components/ToolTimelineBlock.tsx @@ -1,5 +1,7 @@ +import WorktreeActions from '../../../components/worktree/WorktreeActions'; import { useT } from '../../../lib/i18n/I18nContext'; import type { SubagentActivity, ToolTimelineEntry } from '../../../store/chatRuntimeSlice'; +import { basename } from '../../../utils/pathUtils'; import { formatTimelineEntry, formatToolName } from '../../../utils/toolTimelineFormatting'; import { parseWorkerThreadRef } from '../utils/workerThreadRef'; import { agentNameTone, AgentTimelineRail } from './AgentTimelineRail'; @@ -149,6 +151,40 @@ export function SubagentActivityBlock({ {preview}
) : null} + {subagent.worktreePath ? ( +
+
+ + {t('worktree.label')} + + + {basename(subagent.worktreePath)} + + {subagent.isDirty ? ( + + {t('worktree.dirty')} + + ) : ( + + {t('worktree.clean')} + + )} + {subagent.changedFiles && subagent.changedFiles.length > 0 ? ( + + {subagent.changedFiles.length}{' '} + {subagent.changedFiles.length === 1 + ? t('worktree.changedFile') + : t('worktree.changedFiles')} + + ) : null} +
+ +
+ ) : null} {onView ? (