Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 81 additions & 1 deletion apps/mobile/src/components/agents/part-renderer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { describe, expect, it, vi } from 'vitest';

import { type SessionModelOption } from '@/lib/hooks/use-session-model-options';

import { PartRenderer } from './part-renderer';
import { PartRenderer, patchPartFileLabel } from './part-renderer';
import { ReasoningPartRenderer } from './reasoning-part-renderer';
import { TextPartRenderer } from './text-part-renderer';
import { PatchToolCardBody } from './tool-cards/patch-tool-card';
Expand Down Expand Up @@ -303,6 +303,14 @@ describe('PartRenderer', () => {
});

describe('PartRenderer patch part summary', () => {
// `/workspace/<userId>/sessions/<sessionId>` with the repo cloned at that
// root (services/cloud-agent-next/src/workspace.ts:202).
const WORKSPACE_ROOT = '/workspace/a7e4d40b-c28c-4df1-9a1e-f88e7eb467f1/sessions/W1s2';
// `/workspace/<userId>/worktrees/<worktreeId>` is the root a worktree-backed
// session clones into instead (services/cloud-agent-next/src/workspace.ts:211).
const WORKTREE_ROOT =
'/workspace/a7e4d40b-c28c-4df1-9a1e-f88e7eb467f1/worktrees/worktree_9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d';

it('renders the file count and paths for a patch part', () => {
const part = makePatchPart(['src/a.ts', 'src/b.ts']);
// eslint-disable-next-line new-cap
Expand All @@ -320,6 +328,78 @@ describe('PartRenderer patch part summary', () => {
expect(findText(result, 'Updated 1 file')).toHaveLength(1);
});

it('shows the repo-relative file name for an absolute workspace path', () => {
const part = makePatchPart([`${WORKSPACE_ROOT}/README.md`]);
// eslint-disable-next-line new-cap
const result = PartRenderer({ part });
expect(result).not.toBeNull();
expect(findText(result, 'README.md')).toHaveLength(1);
expect(
findAll(
result,
el =>
el.type === 'Text' &&
String((el.props as { children?: unknown }).children).includes('/workspace/')
)
).toHaveLength(0);
});

it('keeps the nested repo-relative path for an absolute workspace path', () => {
const part = makePatchPart([`${WORKSPACE_ROOT}/src/cli.test.ts`]);
// eslint-disable-next-line new-cap
const result = PartRenderer({ part });
expect(findText(result, 'src/cli.test.ts')).toHaveLength(1);
});

it('strips an org-scoped workspace prefix', () => {
const part = makePatchPart(['/workspace/org-1/user-1/sessions/W1s2/src/a.ts']);
// eslint-disable-next-line new-cap
const result = PartRenderer({ part });
expect(findText(result, 'src/a.ts')).toHaveLength(1);
});

it('strips a worktree workspace prefix', () => {
const part = makePatchPart([`${WORKTREE_ROOT}/README.md`]);
// eslint-disable-next-line new-cap
const result = PartRenderer({ part });
expect(result).not.toBeNull();
expect(findText(result, 'README.md')).toHaveLength(1);
expect(
findAll(
result,
el =>
el.type === 'Text' &&
String((el.props as { children?: unknown }).children).includes('/workspace/')
)
).toHaveLength(0);
});

it('strips an org-scoped worktree workspace prefix from a nested path', () => {
expect(
patchPartFileLabel('/workspace/org-1/user-1/worktrees/worktree_9b1deb4d/src/nested/a.ts')
).toBe('src/nested/a.ts');
});

it('renders each file row as a single middle-ellipsized line', () => {
const part = makePatchPart([`${WORKSPACE_ROOT}/src/a.ts`]);
// eslint-disable-next-line new-cap
const result = PartRenderer({ part });
const [fileRow] = findAll(
result,
el => el.type === 'Text' && (el.props as { numberOfLines?: number }).numberOfLines === 1
);
expect(fileRow).toBeDefined();
expect(fileRow?.props).toMatchObject({ numberOfLines: 1, ellipsizeMode: 'middle' });
});

it('leaves a path without the workspace prefix untouched', () => {
expect(patchPartFileLabel('src/a.ts')).toBe('src/a.ts');
});

it('strips the absolute workspace prefix from a nested path', () => {
expect(patchPartFileLabel(`${WORKSPACE_ROOT}/src/nested/a.ts`)).toBe('src/nested/a.ts');
});

it('returns null for a patch part with no files', () => {
const part = makePatchPart([]);
// eslint-disable-next-line new-cap
Expand Down
26 changes: 24 additions & 2 deletions apps/mobile/src/components/agents/part-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@ import { TextPartRenderer } from './text-part-renderer';
import { ToolPartRenderer } from './tool-part-renderer';
import { type OpenChildSession } from './child-session-section';

/**
* The absolute cloud-agent workspace prefixes a patch path can carry:
* `/workspace/<userId>/sessions/<sessionId>/` for a session workspace and
* `/workspace/<userId>/worktrees/<worktreeId>/` for a worktree-backed session
* (either optionally with an organization segment before the user id) — the
* repo is cloned at that root, so dropping it leaves the repo-relative path a
* reader wants instead of the workspace ids.
*
* services/cloud-agent-next/src/workspace.ts:202,211,371
*/
const CLOUD_AGENT_WORKSPACE_PREFIX =
/^\/workspace\/(?:[^/]+\/)?[^/]+\/(?:sessions|worktrees)\/[^/]+\//;

export function patchPartFileLabel(path: string): string {
return path.replace(CLOUD_AGENT_WORKSPACE_PREFIX, '');
}

type PartRendererProps = {
part: Part;
isStreaming?: boolean;
Expand Down Expand Up @@ -111,8 +128,13 @@ export function PartRenderer({
<View className="my-1 gap-1">
<Text className="text-xs text-muted-foreground">{summary}</Text>
{part.files.map(file => (
<Text key={file} className="font-mono text-xs text-muted-foreground" numberOfLines={1}>
{file}
<Text
key={file}
className="font-mono text-xs text-muted-foreground"
numberOfLines={1}
ellipsizeMode="middle"
>
{patchPartFileLabel(file)}
</Text>
))}
</View>
Expand Down
Loading