Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1142cab
chore(mobile): ignore generated sentry.options.json
iscekic Aug 17, 2026
9b33dcf
fix(mobile): surface terminal session errors with Copy and no fake Retry
iscekic Aug 17, 2026
63dba4e
fix(mobile): show unavailable state instead of first-use promo on act…
iscekic Aug 17, 2026
4f1d666
fix(mobile): confirm new-session discard and stay on failed clear
iscekic Aug 17, 2026
ed200c4
feat(mobile): persist security dismiss drafts and show a retry card
iscekic Aug 17, 2026
ea86f7c
feat(mobile): show the code reviewer action-required banner
iscekic Aug 18, 2026
9a353a9
feat(mobile): reorder review detail to outcome-first
iscekic Aug 18, 2026
244a1ac
fix(web): treat a lost retrigger CAS as a conflict
iscekic Aug 18, 2026
884da01
feat(mobile): show the effective agent profile on new session
iscekic Aug 18, 2026
e910cc9
fix(security): refuse enabling with no effective repos
iscekic Aug 18, 2026
8d7f640
feat(mobile): add three-way connectivity status
iscekic Aug 18, 2026
a041411
test(web): fix retrigger parallel test type predicate
iscekic Aug 18, 2026
c8dc11d
feat(web): move org invite email to an outbox
iscekic Aug 18, 2026
523e389
feat(mobile): add a Leave/Commit choice to new session
iscekic Aug 18, 2026
36e0523
feat(security): CAS the security config save with revision admission
iscekic Aug 18, 2026
aeed917
feat(mobile): add a persisted mutation outbox for safe-retry replay
iscekic Aug 18, 2026
2871b3d
fix(mobile): flatten the security config union for optimistic updates
iscekic Aug 18, 2026
0131ab8
fix(storybook): add emailStatus to the invited-member mock
iscekic Aug 18, 2026
ad50f43
refactor(mobile): flatten the repo-status ternary and simplify guards
iscekic Aug 18, 2026
d4d2b2a
chore(format): normalize line wrapping
iscekic Aug 18, 2026
06ff45c
fix(mobile): reuse the stored outbox key on continue and sync
iscekic Aug 18, 2026
5082220
test(mobile): mock the outbox in the findings wiring test
iscekic Aug 18, 2026
7c0b995
refactor(mobile): drop dead session-submit and terminal-error fields
iscekic Aug 18, 2026
a8a8f34
refactor(mobile): unexport the private outbox taxonomy
iscekic Aug 18, 2026
788191c
fix(mobile): keep observed queries on permission denial
iscekic Aug 18, 2026
dcb52ca
fix(mobile): read gate threshold defensively for legacy rows
iscekic Aug 18, 2026
e8832f1
fix(mobile): refresh outbox on empty-state sync settle
iscekic Aug 18, 2026
2384a93
fix(db): fence invite-email claim and reset on validity and status
iscekic Aug 18, 2026
2728351
chore: retrigger review
iscekic Aug 18, 2026
09122c2
Merge branch 'main' into audit-w4a-state-honesty-837d
iscekic Aug 18, 2026
2ace7c8
chore: retrigger review
iscekic Aug 18, 2026
26d8b18
Merge branch 'main' into audit-w4a-state-honesty-837d
iscekic Aug 18, 2026
38c1f94
fix(review): commit the activation boundary in the config transaction…
iscekic Aug 18, 2026
6758309
fix(mobile): keep the outbox load generation-fenced when it marks loaded
iscekic Aug 18, 2026
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
1 change: 1 addition & 0 deletions ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ Manage shared web env var additions and rotations with `pnpm web:env set <VARIAB
- `ABUSE_SERVICE_CF_ACCESS_CLIENT_ID` - Cloudflare Access client ID for abuse service. [PUBLIC]
- `ABUSE_SERVICE_CF_ACCESS_CLIENT_SECRET` - Cloudflare Access client secret for abuse service. `[SECRET]`
- `CRON_SECRET` - Shared secret for authenticated cron endpoints; used in `dev/discord-gateway-cron.ts` and `.env.test`. `[SECRET]`
- `dispatch-invite-email-outbox` - Vercel cron path (`/api/cron/dispatch-invite-email-outbox`) that drains the organization invite-email outbox; reuses `CRON_SECRET` for auth. [SERVER]
- `WORKOS_API_KEY` - WorkOS API key for enterprise SSO. `[SECRET]`
- `WORKOS_CLIENT_ID` - WorkOS client ID for enterprise SSO. [PUBLIC]

Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,6 @@ sentry.options.json
.superpowers/

.env.local

# generated by first-time expo prebuild
sentry.options.json
61 changes: 45 additions & 16 deletions apps/mobile/src/app/(app)/agent-chat/[session-id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
import { SessionConnectionIndicator } from '@/components/agents/session-connection-indicator';
import { SessionContextMetrics } from '@/components/agents/session-context-metrics';
import { AgentSessionProvider } from '@/components/agents/session-provider';
import { buildTerminalErrorCopyText } from '@/components/agents/session-terminal-error';
import { performCopy } from '@/components/agents/use-message-copy';
import { QueryError } from '@/components/query-error';
import { ScreenHeader } from '@/components/screen-header';
import { Button } from '@/components/ui/button';
Expand Down Expand Up @@ -107,32 +109,59 @@ export default function SessionDetailScreen() {
}

if (routeOrganizationId === undefined && sessionQuery.isError) {
// A NOT_FOUND (e.g. the stored session was deleted) can't be recovered by
// retrying — show a permanent "not available" state with no Retry. Other
// errors stay transient and retriable.
const notFound = sessionQuery.error.data?.code === 'NOT_FOUND';
// A NOT_FOUND (e.g. the stored session was deleted) or UNAUTHORIZED
// (org-access denial) can't be recovered by retrying — show a permanent
// state with no Retry. Other errors stay transient and retriable. All
// get Back and Copy.
const errorCode = sessionQuery.error.data?.code;
const notFound = errorCode === 'NOT_FOUND';
const unauthorized = errorCode === 'UNAUTHORIZED';
let title = 'Could not load session';
let message = 'Failed to load session details';
let variant: 'not-found' | 'permission' | 'server' = 'server';
if (notFound) {
title = 'Not found';
message = 'This item may have been removed or is no longer available.';
variant = 'not-found';
} else if (unauthorized) {
title = 'Access denied';
message = "You don't have permission to view this.";
variant = 'permission';
}
const copyText = buildTerminalErrorCopyText(sessionId, title, message);
return (
<View className="flex-1 bg-background">
<ScreenHeader title="Session" />
<SessionConnectionIndicator />
<View className="flex-1 items-center justify-center gap-3 px-6">
<QueryError
variant={notFound ? 'not-found' : 'server'}
variant={variant}
placement="top"
className="px-0 pt-0"
title={notFound ? undefined : 'Could not load session'}
message={notFound ? undefined : 'Failed to load session details'}
onRetry={notFound ? undefined : () => void sessionQuery.refetch()}
title={title}
message={message}
onRetry={notFound || unauthorized ? undefined : () => void sessionQuery.refetch()}
isRetrying={sessionQuery.isFetching}
/>
<Button
variant="ghost"
onPress={() => {
router.replace('/(app)/(tabs)/(2_agents)' as Href);
}}
>
<Text>Back to sessions</Text>
</Button>
<View className="flex-row gap-3">
<Button
variant="ghost"
accessibilityLabel="Copy error details"
onPress={() => {
void performCopy(copyText);
}}
>
<Text>Copy</Text>
</Button>
<Button
variant="ghost"
onPress={() => {
router.replace('/(app)/(tabs)/(2_agents)' as Href);
}}
>
<Text>Back to sessions</Text>
</Button>
</View>
</View>
</View>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { describe, expect, it, vi } from 'vitest';

import {
type AssistantMessage,
type Part,
type StoredMessage,
type UserMessage,
} from '@kilocode/cloud-agent-sdk';

import { buildContinuationSeed } from '@/components/agents/continuation-seed';

vi.mock('@/components/ui/icons', () => ({
Bug: 'Bug',
Code: 'Code',
HelpCircle: 'HelpCircle',
NotebookPen: 'NotebookPen',
Workflow: 'Workflow',
}));

// ---------------------------------------------------------------------------
// Fixtures (minimal, mirrors continuation-seed.test.ts)
// ---------------------------------------------------------------------------

function userInfo(overrides: Partial<UserMessage> = {}): UserMessage {
return {
id: 'u-1',
sessionID: 'ses-1',
role: 'user',
time: { created: 1_700_000_000_000 },
agent: 'build',
model: { providerID: 'kilo', modelID: 'test-model' },
...overrides,
};
}

function assistantInfo(overrides: Partial<AssistantMessage> = {}): AssistantMessage {
return {
id: 'a-1',
sessionID: 'ses-1',
role: 'assistant',
time: { created: 1_700_000_000_000 },
parentID: 'u-1',
modelID: 'test-model',
providerID: 'kilo',
mode: 'code',
agent: 'build',
path: { cwd: '/', root: '/' },
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
...overrides,
};
}

function textPart(text: string, overrides: Partial<Part> = {}): Part {
return {
id: 'p-text',
sessionID: 'ses-1',
messageID: 'msg-1',
type: 'text',
text,
...overrides,
} as unknown as Part;
}

function storedMessage(info: UserMessage | AssistantMessage, parts: Part[] = []): StoredMessage {
return { info, parts };
}

// ---------------------------------------------------------------------------
// Continue-seed provenance: the seed is the visible transcript, never a hidden
// composer buffer.
//
// Structural reason this path is impossible: `useContinueSession.continueSession`
// (use-continue-session.ts) builds the seed from
// `store.get(manager.atoms.messagesList)` — the submitted transcript — and
// passes it to `buildContinuationSeed`. It never reads the new-session draft,
// which lives under `NEW_SESSION_DRAFT_KEY` in the encrypted KV store and is
// read only by `loadDraft`/`useFencedDraftLoad` in `new.tsx`. The two data
// sources are disjoint, so a cleared or unseen draft — text that exists only
// in the composer/KV store and was never submitted into `messagesList` — can
// never become the continue seed.
//
// The tests below pin the consequence: `buildContinuationSeed` derives its
// body exactly from the `messages` argument, so no text outside the transcript
// can be injected.
// ---------------------------------------------------------------------------

describe('continue seed provenance (draft cannot seed)', () => {
it('returns null for an empty transcript, so a draft alone cannot seed', () => {
expect(buildContinuationSeed([])).toBeNull();
});

it('derives the seed body exactly from the visible transcript', () => {
const messages: StoredMessage[] = [
storedMessage(userInfo(), [textPart('visible user turn')]),
storedMessage(assistantInfo(), [textPart('visible assistant reply')]),
];

const seed = buildContinuationSeed(messages);
expect(seed).not.toBeNull();
// eslint-disable-next-line typescript-eslint/no-non-null-assertion -- guarded by expect above
const s = seed!;

// The seed is the fixed preamble plus the serialized transcript. The body
// (everything after the preamble) must be exactly the transcript turns —
// no draft text, no hidden composer buffer can be injected.
const bodyStart = s.indexOf('User:\n');
expect(bodyStart).toBeGreaterThan(0);
expect(s.slice(bodyStart)).toBe(
'User:\nvisible user turn\n\nAssistant:\nvisible assistant reply'
);
});

it('derives the seed body exactly from a transcript with a draft-like string absent', () => {
// A draft string that is not part of any submitted message must not appear
// in the seed, because the seed body is exactly the transcript.
const draftText = 'text the user discarded from the composer';
const messages: StoredMessage[] = [storedMessage(userInfo(), [textPart('only this turn')])];

const seed = buildContinuationSeed(messages);
expect(seed).not.toBeNull();
// eslint-disable-next-line typescript-eslint/no-non-null-assertion -- guarded by expect above
const s = seed!;

const bodyStart = s.indexOf('User:\n');
expect(bodyStart).toBeGreaterThan(0);
// Exact body: the transcript turn only, so the discarded draft cannot appear.
expect(s.slice(bodyStart)).toBe('User:\nonly this turn');
expect(s).not.toContain(draftText);
});
});
Loading
Loading