diff --git a/mobile/app/h/[hostId]/files/preview/[worktreeId].tsx b/mobile/app/h/[hostId]/files/preview/[worktreeId].tsx
index 65412d51958..df60f8fc117 100644
--- a/mobile/app/h/[hostId]/files/preview/[worktreeId].tsx
+++ b/mobile/app/h/[hostId]/files/preview/[worktreeId].tsx
@@ -10,6 +10,11 @@ export default function MobileFilePreviewRoute() {
source?: string | string[]
absolutePath?: string | string[]
grantId?: string | string[]
+ terminal?: string | string[]
+ pathText?: string | string[]
+ cwd?: string | string[]
+ nativeChatTab?: string | string[]
+ nativeChatSession?: string | string[]
line?: string | string[]
column?: string | string[]
name?: string | string[]
diff --git a/mobile/app/h/[hostId]/session/[worktreeId].tsx b/mobile/app/h/[hostId]/session/[worktreeId].tsx
index cf0f1dd7edb..aa59f6df7cb 100644
--- a/mobile/app/h/[hostId]/session/[worktreeId].tsx
+++ b/mobile/app/h/[hostId]/session/[worktreeId].tsx
@@ -3085,6 +3085,10 @@ export default function SessionScreen() {
hostId,
worktreeId,
worktreeName: routeWorktreeName,
+ nativeChatSessionId:
+ activeSessionTab?.type === 'terminal'
+ ? (activeSessionTab.agentStatus?.providerSession?.id ?? null)
+ : null,
activeHandleRef,
terminalCwdRef,
openBrowser: (url) => void handleCreateBrowserRef.current?.(url),
diff --git a/mobile/src/files/mobile-file-preview-request.test.ts b/mobile/src/files/mobile-file-preview-request.test.ts
index f7f77fabd19..48bd26a71db 100644
--- a/mobile/src/files/mobile-file-preview-request.test.ts
+++ b/mobile/src/files/mobile-file-preview-request.test.ts
@@ -179,6 +179,37 @@ describe('mobile-file-preview-request', () => {
})
})
+ it('refreshes a native-chat artifact grant with the same transcript provenance', async () => {
+ const client = clientWithResponses([
+ fail('terminal_file_grant_expired'),
+ ok({
+ exists: true,
+ isDirectory: false,
+ openTarget: {
+ kind: 'absolute-file',
+ absolutePath: '/Users/ada/orca-plans/result.html',
+ grantId: 'grant-2'
+ }
+ }),
+ ok({ content: '
Result
', truncated: false, byteLength: 15 })
+ ])
+
+ await loadMobileFilePreview(client, {
+ source: 'terminalArtifact',
+ worktreeId: 'wt-1',
+ absolutePath: '/Users/ada/orca-plans/result.html',
+ grantId: 'grant-1',
+ pathText: '~/orca-plans/result.html',
+ nativeChatContext: { tabId: 'tab-1', sessionId: 'session-1' }
+ })
+
+ expect(client.sendRequest).toHaveBeenNthCalledWith(2, 'files.resolveTerminalPath', {
+ worktree: 'id:wt-1',
+ pathText: '~/orca-plans/result.html',
+ nativeChatContext: { tabId: 'tab-1', sessionId: 'session-1' }
+ })
+ })
+
it.each([
['terminal_file_grant_expired'],
['terminal_file_grant_mismatch'],
diff --git a/mobile/src/files/mobile-file-preview-route.test.ts b/mobile/src/files/mobile-file-preview-route.test.ts
index ac1888ed46f..784313464ee 100644
--- a/mobile/src/files/mobile-file-preview-route.test.ts
+++ b/mobile/src/files/mobile-file-preview-route.test.ts
@@ -65,6 +65,8 @@ describe('mobile-file-preview-route', () => {
terminal: 'term-1',
pathText: 'result.json',
cwd: '/tmp/run',
+ nativeChatTab: 'tab-1',
+ nativeChatSession: 'session-1',
line: '12',
column: '3'
})
@@ -80,6 +82,8 @@ describe('mobile-file-preview-route', () => {
terminal: 'term-1',
pathText: 'result.json',
cwd: '/tmp/run',
+ nativeChatTab: 'tab-1',
+ nativeChatSession: 'session-1',
line: '12',
column: '3'
}
diff --git a/mobile/src/files/mobile-file-preview-route.ts b/mobile/src/files/mobile-file-preview-route.ts
index 1b277de7faa..f0e7dfbbf49 100644
--- a/mobile/src/files/mobile-file-preview-route.ts
+++ b/mobile/src/files/mobile-file-preview-route.ts
@@ -10,6 +10,8 @@ export type MobileFilePreviewRouteParams = {
terminal?: string
pathText?: string
cwd?: string
+ nativeChatTab?: string
+ nativeChatSession?: string
line?: string
column?: string
name?: string
@@ -35,6 +37,8 @@ type RawPreviewRouteParams = {
terminal?: MobileFilePreviewParamValue
pathText?: MobileFilePreviewParamValue
cwd?: MobileFilePreviewParamValue
+ nativeChatTab?: MobileFilePreviewParamValue
+ nativeChatSession?: MobileFilePreviewParamValue
line?: MobileFilePreviewParamValue
column?: MobileFilePreviewParamValue
name?: MobileFilePreviewParamValue
@@ -76,6 +80,8 @@ export function normalizeMobileFilePreviewRouteParams(
terminal: optionalSingleParam(params.terminal),
pathText: optionalSingleParam(params.pathText),
cwd: optionalSingleParam(params.cwd),
+ nativeChatTab: optionalSingleParam(params.nativeChatTab),
+ nativeChatSession: optionalSingleParam(params.nativeChatSession),
line: optionalSingleParam(params.line),
column: optionalSingleParam(params.column),
name: optionalSingleParam(params.name),
diff --git a/mobile/src/files/mobile-file-preview-source.ts b/mobile/src/files/mobile-file-preview-source.ts
index b1b23757a6e..ae1c846ccfc 100644
--- a/mobile/src/files/mobile-file-preview-source.ts
+++ b/mobile/src/files/mobile-file-preview-source.ts
@@ -15,7 +15,15 @@ export function previewSourceFromRoute(
grantId: params.grantId,
...(params.terminal ? { terminalHandle: params.terminal } : {}),
...(params.pathText ? { pathText: params.pathText } : {}),
- ...(params.cwd ? { cwd: params.cwd } : {})
+ ...(params.cwd ? { cwd: params.cwd } : {}),
+ ...(params.nativeChatTab && params.nativeChatSession
+ ? {
+ nativeChatContext: {
+ tabId: params.nativeChatTab,
+ sessionId: params.nativeChatSession
+ }
+ }
+ : {})
}
}
if (!params.relativePath) {
@@ -28,12 +36,12 @@ export function sourceKeyForPreview(source: MobileFilePreviewSource | null): str
if (!source) {
return null
}
- return source.source === 'terminalArtifact'
- ? JSON.stringify([
- 'terminal',
- source.worktreeId,
- source.absolutePath,
- source.terminalHandle ?? ''
- ])
- : JSON.stringify(['worktree', source.worktreeId, source.relativePath])
+ if (source.source !== 'terminalArtifact') {
+ return JSON.stringify(['worktree', source.worktreeId, source.relativePath])
+ }
+ const key = ['terminal', source.worktreeId, source.absolutePath, source.terminalHandle ?? '']
+ if (source.nativeChatContext) {
+ key.push(source.nativeChatContext.tabId, source.nativeChatContext.sessionId)
+ }
+ return JSON.stringify(key)
}
diff --git a/mobile/src/files/mobile-terminal-artifact-grant-refresh.ts b/mobile/src/files/mobile-terminal-artifact-grant-refresh.ts
index a8864119be4..6e7d4329413 100644
--- a/mobile/src/files/mobile-terminal-artifact-grant-refresh.ts
+++ b/mobile/src/files/mobile-terminal-artifact-grant-refresh.ts
@@ -1,3 +1,4 @@
+import type { RuntimeNativeChatFileContext } from '../../../src/shared/runtime-types'
import type { RpcClient } from '../transport/rpc-client'
import type { RpcFailure, RpcResponse, RpcSuccess } from '../transport/types'
import { isTerminalArtifactGrantError } from './terminal-artifact-grant-error'
@@ -12,6 +13,7 @@ export type MobileTerminalArtifactPreviewSource = {
terminalHandle?: string
pathText?: string
cwd?: string
+ nativeChatContext?: RuntimeNativeChatFileContext
}
export type TerminalArtifactRetryOptions = {
@@ -32,7 +34,8 @@ export async function refreshTerminalArtifactSourceAfterGrantFailure(
worktree: `id:${source.worktreeId}`,
pathText: source.pathText ?? source.absolutePath,
...(source.cwd ? { cwd: source.cwd } : {}),
- ...(source.terminalHandle ? { terminal: source.terminalHandle } : {})
+ ...(source.terminalHandle ? { terminal: source.terminalHandle } : {}),
+ ...(source.nativeChatContext ? { nativeChatContext: source.nativeChatContext } : {})
})
if (!refreshed.ok) {
return null
@@ -51,7 +54,8 @@ export async function refreshTerminalArtifactSourceAfterGrantFailure(
grantId: result.openTarget.grantId,
...(source.terminalHandle ? { terminalHandle: source.terminalHandle } : {}),
...(source.pathText ? { pathText: source.pathText } : {}),
- ...(source.cwd ? { cwd: source.cwd } : {})
+ ...(source.cwd ? { cwd: source.cwd } : {}),
+ ...(source.nativeChatContext ? { nativeChatContext: source.nativeChatContext } : {})
}
}
diff --git a/mobile/src/session/mobile-file-tap-open.ts b/mobile/src/session/mobile-file-tap-open.ts
index 627d9a5ff39..13c306af649 100644
--- a/mobile/src/session/mobile-file-tap-open.ts
+++ b/mobile/src/session/mobile-file-tap-open.ts
@@ -1,5 +1,6 @@
import type {
RuntimeFileOpenResult,
+ RuntimeNativeChatFileContext,
RuntimeTerminalPathResolution
} from '../../../src/shared/runtime-types'
import { filesystemPathToFileUri } from '../../../src/shared/file-uri-path'
@@ -22,6 +23,7 @@ export type OpenMobileFileTapOptions = {
terminalHandle?: string | null
pathText: string
cwd?: string | null
+ nativeChatContext?: RuntimeNativeChatFileContext | null
line: number | null
column: number | null
pushPreviewRoute: (href: ReturnType) => void
@@ -80,7 +82,8 @@ async function openMobileFileTapAsync(
...(options.terminalHandle && options.terminalHandle.trim().length > 0
? { terminal: options.terminalHandle }
: {}),
- ...(options.cwd && options.cwd.trim().length > 0 ? { cwd: options.cwd } : {})
+ ...(options.cwd && options.cwd.trim().length > 0 ? { cwd: options.cwd } : {}),
+ ...(options.nativeChatContext ? { nativeChatContext: options.nativeChatContext } : {})
},
{ timeoutMs: 10_000 }
)
@@ -116,6 +119,12 @@ async function openMobileFileTapAsync(
...(options.terminalHandle && options.terminalHandle.trim().length > 0
? { terminal: options.terminalHandle }
: {}),
+ ...(options.nativeChatContext
+ ? {
+ nativeChatTab: options.nativeChatContext.tabId,
+ nativeChatSession: options.nativeChatContext.sessionId
+ }
+ : {}),
name: displayNameFromPath(resolved.openTarget.absolutePath),
...(options.line !== null ? { line: String(options.line) } : {}),
...(options.column !== null ? { column: String(options.column) } : {}),
diff --git a/mobile/src/session/mobile-native-chat-open-file.test.ts b/mobile/src/session/mobile-native-chat-open-file.test.ts
index d44530e5cd0..363edb90218 100644
--- a/mobile/src/session/mobile-native-chat-open-file.test.ts
+++ b/mobile/src/session/mobile-native-chat-open-file.test.ts
@@ -65,6 +65,53 @@ describe('openMobileNativeChatFileTap', () => {
)
})
+ it('carries the native-chat session identity into resolution and grant refresh', async () => {
+ const sendRequest = vi.fn(async () =>
+ ok({
+ worktree: 'wt-1',
+ relativePath: null,
+ absolutePath: '/Users/ada/orca-plans/result.html',
+ exists: true,
+ isDirectory: false,
+ openTarget: {
+ kind: 'absolute-file',
+ provider: 'local',
+ absolutePath: '/Users/ada/orca-plans/result.html',
+ grantId: 'grant-1'
+ }
+ })
+ )
+ const options = baseOptions({ sendRequest })
+
+ openMobileNativeChatFileTap({
+ ...options,
+ pathText: '~/orca-plans/result.html',
+ nativeChatContext: { tabId: 'tab-1', sessionId: 'session-1' }
+ })
+ await Promise.resolve()
+
+ expect(sendRequest).toHaveBeenCalledWith(
+ 'files.resolveTerminalPath',
+ {
+ worktree: 'id:wt-1',
+ pathText: '~/orca-plans/result.html',
+ crossWorkspace: true,
+ nativeChatContext: { tabId: 'tab-1', sessionId: 'session-1' }
+ },
+ { timeoutMs: 10_000 }
+ )
+ expect(options.pushPreviewRoute).toHaveBeenCalledWith({
+ pathname: '/h/[hostId]/files/preview/[worktreeId]',
+ params: expect.objectContaining({
+ source: 'terminalArtifact',
+ absolutePath: '/Users/ada/orca-plans/result.html',
+ grantId: 'grant-1',
+ nativeChatTab: 'tab-1',
+ nativeChatSession: 'session-1'
+ })
+ })
+ })
+
it('parses a :line:col citation and opens the mobile preview route', async () => {
const sendRequest = vi.fn(async () => worktreeFileResolution('src/app.ts'))
const options = baseOptions({ sendRequest })
diff --git a/mobile/src/session/use-mobile-file-tap-handlers.test.ts b/mobile/src/session/use-mobile-file-tap-handlers.test.ts
index 0cfa617f269..21f07562459 100644
--- a/mobile/src/session/use-mobile-file-tap-handlers.test.ts
+++ b/mobile/src/session/use-mobile-file-tap-handlers.test.ts
@@ -34,12 +34,13 @@ describe('useMobileFileTapHandlers', () => {
hostId: 'host-1',
worktreeId: 'wt-1',
worktreeName: 'Orca',
+ nativeChatSessionId: 'session-1',
activeHandleRef: { current: 'terminal-1' as string | null },
terminalCwdRef: { current: new Map([['terminal-1', '/repo/sub']]) },
openBrowser: vi.fn(),
fetchSessionTabs: vi.fn(async () => {}),
getSessionTabs: () => [],
- getActiveSessionTabId: () => null,
+ getActiveSessionTabId: () => 'terminal-tab',
getActiveSessionTabType: () => 'terminal',
switchSessionTab: vi.fn(),
scheduleDelayedAction: vi.fn(),
@@ -131,7 +132,12 @@ describe('useMobileFileTapHandlers', () => {
expect(sendRequest).toHaveBeenCalledWith(
'files.resolveTerminalPath',
- { worktree: 'id:wt-1', pathText: 'mobile/src/x.ts', crossWorkspace: true },
+ {
+ worktree: 'id:wt-1',
+ pathText: 'mobile/src/x.ts',
+ crossWorkspace: true,
+ nativeChatContext: { tabId: 'terminal-tab', sessionId: 'session-1' }
+ },
{ timeoutMs: 10_000 }
)
expect(options.reportChatTapFailure).toHaveBeenCalledWith("Couldn't open mobile/src/x.ts:12")
diff --git a/mobile/src/session/use-mobile-file-tap-handlers.ts b/mobile/src/session/use-mobile-file-tap-handlers.ts
index 276be5b566f..67995115f45 100644
--- a/mobile/src/session/use-mobile-file-tap-handlers.ts
+++ b/mobile/src/session/use-mobile-file-tap-handlers.ts
@@ -10,6 +10,7 @@ type MobileFileTapHandlerOptions = {
hostId: string
worktreeId: string
worktreeName?: string
+ nativeChatSessionId: string | null
activeHandleRef: MutableRefObject
terminalCwdRef: MutableRefObject