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
5 changes: 4 additions & 1 deletion .kilo/skills/kilo-mcp/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ to send platform feedback and bug reports rather than a support channel.
The first path segment names an area. Search to learn an area's vocabulary:
each row's `summary` and `tags` are what the search matches.

**867 procedures** — **373 queries**, **494 mutations** — under **53 prefixes**.
**883 procedures** — **379 queries**, **504 mutations** — under **54 prefixes**.

| Prefix | Procedures | Queries | Mutations |
|---|---:|---:|---:|
Expand All @@ -69,6 +69,7 @@ each row's `summary` and `tags` are what the search matches.
| `cliSessions` | 16 | 7 | 9 |
| `mcpGateway` | 16 | 4 | 12 |
| `providerReview` | 16 | 8 | 8 |
| `todos` | 16 | 6 | 10 |
| `deployments` | 14 | 6 | 8 |
| `moderation` | 11 | 3 | 8 |
| `codeReviews` | 9 | 6 | 3 |
Expand Down Expand Up @@ -115,4 +116,6 @@ The second path segment of each area that splits, with procedure counts:

- `organizations.*` — 301: `kiloclaw` 66, `cloudAgentNext` 31, `securityAgent` 26, `(direct)` 19, `appBuilder` 17, `deployments` 17, `groups` 14, `reviewAgent` 13, `subscription` 12, `kiloPass` 11, `settings` 11, `autoTriage` 8, `autoFix` 7, `members` 7, `bitbucket` 6, `sso` 6, `autoTopUp` 5, `modes` 5, `subOrganizations` 5, `usageDetails` 5, `securityAuditLog` 4, `auditLogs` 3, `funds` 2, `salesDemo` 1

- `todos.*` — 16: `items` 11, `lists` 5

- `codeReviews.*` — 9: `(direct)` 7, `analytics` 2
1 change: 1 addition & 0 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
"react-native-reanimated": "4.7.1",
"react-native-safe-area-context": "5.7.0",
"react-native-screens": "4.26.2",
"react-native-sortables": "^1.10.1",
"react-native-svg": "15.15.4",
"react-native-webview": "13.16.1",
"react-native-worklets": "0.13.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,14 @@ vi.mock('@/components/agents/session-list-screen', () => ({
AgentSessionListScreen: () => null,
}));

vi.mock('@/components/todos/agents-view-segment', () => ({
AgentsViewSegment: () => null,
}));

vi.mock('@/components/todos/todo-lists-screen', () => ({
TodoListsScreen: () => null,
}));

type AlertButton = { text: string; onPress?: () => void };

const noOp = () => undefined;
Expand Down
12 changes: 10 additions & 2 deletions apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { useCallback, useEffect } from 'react';
import { useCallback, useEffect, useState } from 'react';
import * as WebBrowser from 'expo-web-browser';
import { useFocusEffect } from 'expo-router';
import { Alert, AppState, Platform } from 'react-native';

import { i18n } from '@/i18n';
import { AgentSessionListScreen } from '@/components/agents/session-list-screen';
import { type AgentsView, AgentsViewSegment } from '@/components/todos/agents-view-segment';
import { TodoListsScreen } from '@/components/todos/todo-lists-screen';
import { runPendingWidgetActions } from '@/glanceable-ios/widget-actions';
import { getGitHubIntegrationUrl } from '@/lib/agent-github-integration';
import { WEB_BASE_URL } from '@/lib/config';
Expand Down Expand Up @@ -123,6 +125,7 @@ async function retryGitHubInstall(organizationId?: string): Promise<void> {
}

export default function AgentSessionList() {
const [view, setView] = useState<AgentsView>('sessions');
const consumeReturnOutcome = useCallback(() => {
const result = getGitHubInstallReturnOutcome();
const alert = buildGitHubInstallOutcomeAlert(result, () => {
Expand Down Expand Up @@ -161,5 +164,10 @@ export default function AgentSessionList() {
}, [])
);

return <AgentSessionListScreen />;
const segmentControl = <AgentsViewSegment value={view} onChange={setView} />;
return view === 'sessions' ? (
<AgentSessionListScreen segmentControl={segmentControl} />
) : (
<TodoListsScreen segmentControl={segmentControl} />
);
}
3 changes: 3 additions & 0 deletions apps/mobile/src/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ export default function AppLayout() {
<Stack.Screen name="agent-chat/instance-picker" options={sheetOptions} />
<Stack.Screen name="agent-chat/folder-picker" options={sheetOptions} />
<Stack.Screen name="agent-chat/profile-picker" options={sheetOptions} />
<Stack.Screen name="todos/[list-id]" />
<Stack.Screen name="todos/item/[item-id]" />
<Stack.Screen name="todos/list-settings" options={sheetOptions} />
<Stack.Screen name="share-gate" options={sheetOptions} />
<Stack.Screen name="language-picker" options={searchSheetOptions} />
<Stack.Screen name="transcription-model-picker" options={sheetOptions} />
Expand Down
14 changes: 14 additions & 0 deletions apps/mobile/src/app/(app)/todos/[list-id].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { type Href, useLocalSearchParams } from 'expo-router';

import { InvalidRouteState } from '@/components/invalid-route-state';
import { TodoListScreen } from '@/components/todos/todo-list-screen';
import { parseParam } from '@/lib/route-params';

export default function TodoListRoute() {
const { 'list-id': rawListId } = useLocalSearchParams<{ 'list-id': string }>();
const listId = parseParam(rawListId);
if (listId === null) {
return <InvalidRouteState backTo={'/(app)/(tabs)/(2_agents)' as Href} />;
}
return <TodoListScreen listId={listId} />;
}
14 changes: 14 additions & 0 deletions apps/mobile/src/app/(app)/todos/item/[item-id].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { type Href, useLocalSearchParams } from 'expo-router';

import { InvalidRouteState } from '@/components/invalid-route-state';
import { TodoDetailScreen } from '@/components/todos/todo-detail-screen';
import { parseParam } from '@/lib/route-params';

export default function TodoDetailRoute() {
const { 'item-id': rawItemId } = useLocalSearchParams<{ 'item-id': string }>();
const itemId = parseParam(rawItemId);
if (itemId === null) {
return <InvalidRouteState backTo={'/(app)/(tabs)/(2_agents)' as Href} />;
}
return <TodoDetailScreen itemId={itemId} />;
}
17 changes: 17 additions & 0 deletions apps/mobile/src/app/(app)/todos/list-settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useLocalSearchParams } from 'expo-router';

import { TodoListSettingsSheet } from '@/components/todos/todo-list-settings-sheet';
import { parseParam } from '@/lib/route-params';

export default function TodoListSettingsRoute() {
const { listId, organizationId } = useLocalSearchParams<{
listId?: string;
organizationId?: string;
}>();
return (
<TodoListSettingsSheet
listId={parseParam(listId)}
organizationId={parseParam(organizationId)}
/>
);
}
24 changes: 24 additions & 0 deletions apps/mobile/src/components/agents/new-session-prefill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,17 @@ describe('appendNewSessionPrefill', () => {
'/agent-chat/new?prefillRepo=owner%2Frepo&prefillModel=kilo-auto%2Fefficient'
);
});

it('appends the prompt and the to-do id after the targets', () => {
const result = appendNewSessionPrefill('/agent-chat/new', {
repo: 'owner/repo',
prompt: 'Fix login\n\nSee #12 & retry',
todoId: 'todo-1',
});
expect(result).toBe(
'/agent-chat/new?prefillRepo=owner%2Frepo&prefillPrompt=Fix%20login%0A%0ASee%20%2312%20%26%20retry&todoId=todo-1'
);
});
});

// ════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -181,6 +192,19 @@ describe('readNewSessionPrefill', () => {
const prefill = readNewSessionPrefill({ prefillRepo: undefined, prefillMode: undefined });
expect(prefill).toEqual({ mode: 'code' });
});

it('reads the prompt and the to-do id', () => {
const prefill = readNewSessionPrefill({
prefillPrompt: 'Fix login\n\nnotes',
todoId: ['todo-1', 'todo-2'],
});
expect(prefill).toEqual({ mode: 'code', prompt: 'Fix login\n\nnotes', todoId: 'todo-1' });
});

it('treats a blank prompt and an empty to-do id as absent', () => {
const prefill = readNewSessionPrefill({ prefillPrompt: ' \n ', todoId: '' });
expect(prefill).toEqual({ mode: 'code' });
});
});

// ════════════════════════════════════════════════════════════════
Expand Down
15 changes: 15 additions & 0 deletions apps/mobile/src/components/agents/new-session-prefill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export type NewSessionPrefillParams = {
mode?: string;
model?: string;
variant?: string;
prompt?: string;
/** The to-do the started session links to. */
todoId?: string;
};

export type NewSessionPrefill = {
Expand All @@ -15,6 +18,8 @@ export type NewSessionPrefill = {
repo?: string;
model?: string;
variant?: string;
prompt?: string;
todoId?: string;
};

function isValidOwnerRepo(value: string): boolean {
Expand Down Expand Up @@ -89,6 +94,12 @@ export function appendNewSessionPrefill(base: string, params: NewSessionPrefillP
if (params.variant) {
entries.push(['prefillVariant', params.variant]);
}
if (params.prompt) {
entries.push(['prefillPrompt', params.prompt]);
}
if (params.todoId) {
entries.push(['todoId', params.todoId]);
}

if (entries.length === 0) {
return base;
Expand Down Expand Up @@ -204,12 +215,16 @@ export function readNewSessionPrefill(
const rawRepo = getFirstParam(raw, 'prefillRepo');
const rawModel = getFirstParam(raw, 'prefillModel');
const rawVariant = getFirstParam(raw, 'prefillVariant');
const rawPrompt = getFirstParam(raw, 'prefillPrompt');
const rawTodoId = getFirstParam(raw, 'todoId');

return {
mode: normalizeAgentMode(rawMode || undefined),
...(rawRepo ? { repo: rawRepo } : {}),
...(rawModel ? { model: rawModel } : {}),
...(rawVariant ? { variant: rawVariant } : {}),
...(rawPrompt.trim() ? { prompt: rawPrompt } : {}),
...(rawTodoId ? { todoId: rawTodoId } : {}),
};
}

Expand Down
54 changes: 45 additions & 9 deletions apps/mobile/src/components/agents/new-session-screen-body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { View } from 'react-native';
import { useTranslation } from 'react-i18next';
import { type Href, useLocalSearchParams, useNavigation, useRouter } from 'expo-router';
import { useActionSheet } from '@expo/react-native-action-sheet';
import { useQuery } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner-native';
import { type KiloSessionId, type RemoteModelOverride } from '@kilocode/cloud-agent-sdk';

Expand All @@ -25,7 +25,10 @@ import { isCloudPrepareRetryableError } from '@/components/agents/mobile-session
import { useEffectiveProfileCustomModes } from '@/components/agents/use-effective-profile-custom-modes';
import { useNewSessionModelState } from '@/components/agents/new-session-model-provider';
import { pickAgentAttachments } from '@/components/agents/attachment-picker';
import { useNewSessionPrefillTargets } from '@/components/agents/use-new-session-prefill';
import {
useNewSessionPrefill,
useNewSessionPrefillTargets,
} from '@/components/agents/use-new-session-prefill';
import {
readCloneFromKiloSessionId,
readCloneSourceTitle,
Expand All @@ -36,6 +39,7 @@ import {
import { useContinueCloudCreate } from '@/components/agents/use-continue-cloud-create';
import { type VariableEdit } from '@/components/profiles/profile-variables-model';
import { ScreenHeader } from '@/components/screen-header';
import { SaveDraftAsTodoSheet } from '@/components/todos/save-draft-as-todo-sheet';
import { Text } from '@/components/ui/text';
import { i18n } from '@/i18n';
import { useNewSessionDiscardGuard } from '@/components/agents/use-new-session-discard-guard';
Expand Down Expand Up @@ -86,6 +90,7 @@ import { peekSharePayload } from '@/lib/share-payload';
import { sessionDisplayTitle } from '@/lib/session-display-title';
import { useNewSessionShareRemote } from '@/lib/use-new-session-share-remote';
import { useNewSessionRepos } from '@/lib/use-new-session-repos';
import { linkStartedTodoSession } from '@/lib/hooks/use-todos';
import { useTRPC } from '@/lib/trpc';
import { settleVoiceInputBeforeSubmit } from '@/lib/voice-input/voice-input-submit';

Expand Down Expand Up @@ -196,9 +201,11 @@ export function NewSessionScreenBody() {

// Share-prefill precedence, resolved once before the prompt mounts. The
// prompt's own `useSharePrefill` still delivers the shared files and
// re-applies the same text idempotently.
// re-applies the same text idempotently. A to-do's Start carries its prompt
// the same way, so it also beats a stored draft.
const { prompt: routePrefillPrompt, todoId } = useNewSessionPrefill();
const [sharePrefillText] = useState<string | null>(() =>
shareId ? (peekSharePayload(shareId)?.text ?? null) : null
shareId ? (peekSharePayload(shareId)?.text ?? null) : (routePrefillPrompt ?? null)
);
// The share prefill is known synchronously and always beats a stored draft,
// so it seeds the first render; the stored draft only exists once the load
Expand Down Expand Up @@ -442,11 +449,22 @@ export function NewSessionScreenBody() {
const handleCloneImportFailure = useCallback((key: string) => {
setCloneImportFailureKey(key);
}, []);

const armCloneNavigateBypass = useCallback(() => {
cloneNavigateBypassRef.current = true;
}, []);

// A to-do start can target a remote instance instead of Cloud Agent; the
// spawned session links to the to-do the same way the cloud path links it.
const queryClient = useQueryClient();
const handleSpawnSucceeded = useCallback(
(sessionId: string) => {
if (todoId) {
void linkStartedTodoSession({ queryClient, trpc, todoId, sessionId });
}
},
[queryClient, todoId, trpc]
);

// The manual draft as the create body carries it: one plaintext value per key
// (the editor already refuses a key that cleans onto an existing one) and the
// non-blank setup commands in list order.
Expand All @@ -473,6 +491,7 @@ export function NewSessionScreenBody() {
profileId,
manualEnvVars,
setupCommands,
todoId,
Comment thread
iscekic marked this conversation as resolved.
Resolved
});

// Seed the route-owned prompt state from the restored draft once the load
Expand Down Expand Up @@ -543,16 +562,13 @@ export function NewSessionScreenBody() {
onSpawnFailed: () => {
skipDiscardGuardRef.current = false;
},
// Clone entry: the dispatch sends the source id (and a fork cut) only when
// the selected CLI can clone it, and surfaces delivered clone/import
// failures through this inline-reason callback.
cloneFromKiloSessionId: isCloneEntry ? cloneFromKiloSessionId : null,
cloneUpToMessageId: cloneUpToMessageId || null,
forkPrefillText: forkPrefillText || null,
onCloneImportFailure: handleCloneImportFailure,
onSpawnReady: armCloneNavigateBypass,
onSpawnSucceeded: handleSpawnSucceeded,
});

const runCloudCreate = useContinueCloudCreate(organizationId, armCloneNavigateBypass);

const handleModelSelect = useCallback(
Expand Down Expand Up @@ -632,10 +648,18 @@ export function NewSessionScreenBody() {
attachments.releaseUnclaimedUploads();
}, [userId, promptRef, attachments]);

// "Save as to-do" keeps the leave the confirm intercepted until a list is
// picked; saving then discards the draft and replays that leave.
const [saveAsTodoLeave, setSaveAsTodoLeave] = useState<(() => void) | null>(null);
const handleSaveAsTodo = useCallback((leave: () => void) => {
setSaveAsTodoLeave(() => leave);
}, []);

const { discardConfirm } = useNewSessionDiscardGuard({
dirty: (isCloneEntry ? false : hasPrompt) || attachments.hasUnclaimedAttachments,
hasUnclaimedAttachments: attachments.hasUnclaimedAttachments,
onDiscard: handleDiscardDraft,
onSaveAsTodo: !isCloneEntry && hasPrompt ? handleSaveAsTodo : undefined,
skipNextGuardRef: skipDiscardGuardRef,
});

Expand Down Expand Up @@ -990,6 +1014,18 @@ export function NewSessionScreenBody() {
{/* The discard confirm: a Modal overlay, so the composer behind it keeps
its layout while the destructive choice keeps its red fill. */}
{discardConfirm}
<SaveDraftAsTodoSheet
visible={saveAsTodoLeave !== null}
organizationId={organizationId ?? null}
getDraft={() => promptRef.current}
onSaved={() => {
saveAsTodoLeave?.();
setSaveAsTodoLeave(null);
}}
onClose={() => {
setSaveAsTodoLeave(null);
}}
/>
</View>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ vi.mock('@/components/agents/context-usage-ring', () => ({
vi.mock('@/components/agents/session-page-sheet', () => ({
SessionPageSheet: 'SessionPageSheet',
}));
vi.mock('@/components/todos/session-todo-link', () => ({
SessionTodoLinkRow: 'SessionTodoLinkRow',
}));

const WARNING_COPY =
'Approve permission asks for this session automatically. Tools then run without a prompt.';
Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/src/components/agents/session-context-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { Row, TokenRow } from './session-detail-rows';
import { SessionPageSheet } from './session-page-sheet';
import { copySessionId, copySessionLink } from './session-row-actions';
import { type SessionConnectionDisplay } from './session-connection-indicator-state';
import { SessionTodoLinkRow } from '@/components/todos/session-todo-link';

type SessionContextSheetProps = {
visible: boolean;
Expand Down Expand Up @@ -338,6 +339,8 @@ export function SessionContextSheet({
</Text>
</Row>

<SessionTodoLinkRow sessionId={sessionId} />

{/* The pill is independent of goal and PR status: the sheet shows the
associated PR in every state (open/draft/merged/closed) whenever
the session has one, unlike the session page which gates on an
Expand Down
Loading
Loading