Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7021b77
feat(mobile): rebuild the chat tab on the harness SDK
iscekic Sep 15, 2026
203212f
chore(kilo-mcp): refresh catalog.json from CI
github-actions[bot] Sep 15, 2026
f5ccfb5
feat(mobile): connect chat to the Kilo MCP server with the signed-in …
iscekic Sep 22, 2026
49ecdd3
Merge remote-tracking branch 'origin/feat/kilo-harness-sdk' into feat…
iscekic Sep 26, 2026
193a363
fix(harness-sdk): keep the library's abort link until the operation ends
iscekic Sep 26, 2026
4225196
fix(harness-sdk): bound the credential by the operation's deadline
iscekic Sep 26, 2026
069cb11
fix(harness-sdk): give a call a deadline above the time it is waited …
iscekic Sep 26, 2026
8464244
fix(mobile): publish the Kilo tools answer that a cache hit serves
iscekic Sep 26, 2026
5718c48
fix(mobile): keep each Kilo tools discovery on its caller's deadline
iscekic Sep 26, 2026
4337859
fix(mobile): lock the Kilo tools switch when there is nothing to use
iscekic Sep 26, 2026
db90d79
Merge remote-tracking branch 'origin/feat/kilo-harness-sdk' into feat…
iscekic Sep 26, 2026
0c0c03f
fix(mobile): pass the segments to the toaster's tab bar check
iscekic Sep 26, 2026
f2f1ea6
test(mobile): read the hoisted query client mock in the account switc…
iscekic Sep 26, 2026
9319f7a
test(mobile): load the real config module in the MCP URL test
iscekic Sep 26, 2026
73292ec
Merge remote-tracking branch 'origin/feat/kilo-harness-sdk' into feat…
iscekic Sep 28, 2026
7e379e2
Merge remote-tracking branch 'origin/feat/kilo-harness-sdk' into feat…
iscekic Oct 10, 2026
73ff145
chore(db): regenerate the quick chat drop migration after the merge
iscekic Oct 10, 2026
f164392
fix(mobile): move the chat screen to react-native-keyboard-controller
iscekic Oct 10, 2026
b9a778f
chore(kilo-mcp): refresh catalog.json and SKILL.md from CI
github-actions[bot] Oct 10, 2026
be0ed92
fix(mobile): confirm a chat delete through the app dialog
iscekic Oct 10, 2026
f779ba7
Merge remote-tracking branch 'origin/feat/mobile-harness-chat' into f…
iscekic Oct 10, 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
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ jobs:
- name: Build trpc types
run: pnpm --filter @kilocode/trpc run build

- name: Build harness SDK
run: pnpm --filter @kilocode/harness-sdk run build

- name: Lint
run: scripts/lint-all.sh

Expand Down Expand Up @@ -480,6 +483,12 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

# apps/mobile tests the app against the built harness SDK, the way it
# already runs against built trpc types.
- name: Build harness SDK
if: matrix.workspace.name == 'kilo-app'
run: pnpm --filter @kilocode/harness-sdk run build

- name: Run tests
run: pnpm --filter ${{ matrix.workspace.name }} test

Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/kilo-app-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ jobs:
- name: Build trpc types
run: pnpm --filter @kilocode/trpc run build

- name: Build harness SDK
run: pnpm --filter @kilocode/harness-sdk run build
Comment thread
iscekic marked this conversation as resolved.
Resolved

- name: Typecheck
run: pnpm --filter kilo-app run typecheck

Expand Down Expand Up @@ -96,6 +99,9 @@ jobs:
- name: Build trpc types
run: pnpm --filter @kilocode/trpc run build

- name: Build harness SDK
run: pnpm --filter @kilocode/harness-sdk run build

- name: Lint
run: pnpm --filter kilo-app run lint

Expand All @@ -122,6 +128,9 @@ jobs:
- name: Build trpc types
run: pnpm --filter @kilocode/trpc run build

- name: Build harness SDK
run: pnpm --filter @kilocode/harness-sdk run build

- name: Check i18n catalogs and leftover copy
run: pnpm --filter kilo-app run check:i18n
- name: Check for silently-dropped NativeWind classes
Expand Down Expand Up @@ -174,6 +183,9 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build harness SDK
run: pnpm --filter @kilocode/harness-sdk run build

- name: Check unused exports
run: pnpm --filter kilo-app run check:unused

Expand Down Expand Up @@ -203,5 +215,8 @@ jobs:
- name: Build trpc types
run: pnpm --filter @kilocode/trpc run build

- name: Build harness SDK
run: pnpm --filter @kilocode/harness-sdk run build

- name: Test
run: pnpm --filter kilo-app run test
6 changes: 6 additions & 0 deletions .github/workflows/mobile-native-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ jobs:
- name: Build trpc types
run: pnpm --filter @kilocode/trpc run build

- name: Build harness SDK
run: pnpm --filter @kilocode/harness-sdk run build

# A consumer computes the hash on macOS; the gate computed it on
# Linux. A mismatch would publish an artifact nobody can ever find,
# so fail loudly instead. Android is checked here too: it builds on
Expand Down Expand Up @@ -217,6 +220,9 @@ jobs:
- name: Build trpc types
run: pnpm --filter @kilocode/trpc run build

- name: Build harness SDK
run: pnpm --filter @kilocode/harness-sdk run build

- name: Expo prebuild
run: CI=1 pnpm --filter kilo-app exec expo prebuild --platform android

Expand Down
1 change: 1 addition & 0 deletions apps/mobile/.env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ SESSION_INGEST_WS_URL=ws://localhost:8800
APPSFLYER_DEV_KEY=jnoVs6KzXanpbKrqXckPu9
APPSFLYER_APP_ID=6761193135
KILO_CHAT_URL=http://localhost:8808
KILO_MCP_URL=http://localhost:8815
EVENT_SERVICE_URL=ws://localhost:8809
NOTIFICATIONS_URL=http://localhost:8804
POSTHOG_API_KEY=phc_GK2Pxl0HPj5ZPfwhLRjXrtdz8eD7e9MKnXiFrOqnB6z
Expand Down
6 changes: 6 additions & 0 deletions apps/mobile/.oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,12 @@
"import/no-nodejs-modules": "off"
}
},
{
"files": ["src/lib/chat/store.test.ts"],
"rules": {
"import/no-nodejs-modules": "off"
}
},
{
"files": ["src/lib/expo-router-linking.mounted.test.tsx"],
"rules": {
Expand Down
12 changes: 12 additions & 0 deletions apps/mobile/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,15 @@ The app follows https://github.com/Kilo-Org/kilo-design/ in general, except wher
## Debugging

Add narrow temporary logs at the real boundaries. Reproduce. Read the tmux service logs. Fix the demonstrated cause. Remove the logs. Do not guess, and do not commit debug logging.

## Chat tab

The Chat tab runs `@kilocode/harness-sdk` on the device. `src/lib/chat/` holds the registry (the conversations that are running), the layers (the plugins they run on) and the store (the list and its CRUD). The SDK saves one conversation; this app owns the list, the ordering, the titles, the deleting and the scoping.

**A conversation can move, and the screen follows the state rather than a return value.** Switching models clones the conversation onto a new session, and the mover is not always the screen: a question typed on another model while an answer was arriving moves the chat from inside the registry. So the chat that was left points at the one it became, and `useChat` follows that.

A chat is opened with the SDK's `time` tool and no other. A model has no clock, so it answers a dated question from its training date, confidently and wrong. The other three tools the SDK ships stay off: the composer is already how a person is asked something, a subagent costs a second session, and a to-do list is working memory for a run a chat does not have.

**Prove a chat change against the same eleven models the SDK's live runs use.** The list is in `packages/harness-sdk/e2e/setup.ts`, and `pnpm --filter @kilocode/harness-sdk test:e2e:time full` is the sweep across all of them. Take one of those models through the app itself on the simulator as well: the sweep proves what the models do, the simulator proves this app's wiring, and neither proves the other. Both cost real money, so run the sweep when the tool set or the system prompt changes, not for a change to a screen.

Reading the model picker's accessibility tree in full takes WebDriverAgent down, and the Appium server with it. Drive it by XPath, never `getPageSource`, and cap the snapshot with `driver.updateSettings({ snapshotMaxDepth: 50 })`.
7 changes: 7 additions & 0 deletions apps/mobile/drizzle/0001_safe_hellfire_club.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE TABLE `chats` (
`session_id` text PRIMARY KEY NOT NULL,
`scope` text NOT NULL,
`updated_at` integer NOT NULL
);
--> statement-breakpoint
CREATE INDEX `chats_scope_updated_at` ON `chats` (`scope`,`updated_at`);
104 changes: 104 additions & 0 deletions apps/mobile/drizzle/meta/0001_snapshot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
{
"version": "6",
"dialect": "sqlite",
"id": "32071253-3838-4db3-aee9-d05b75bc652b",
"prevId": "e7d69526-c2d6-4633-b14f-24bcef818f44",
"tables": {
"chats": {
"name": "chats",
"columns": {
"session_id": {
"name": "session_id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"chats_scope_updated_at": {
"name": "chats_scope_updated_at",
"columns": [
"scope",
"updated_at"
],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"kv": {
"name": "kv",
"columns": {
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"k": {
"name": "k",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"v": {
"name": "v",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"kv_scope_k_pk": {
"columns": [
"scope",
"k"
],
"name": "kv_scope_k_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}
7 changes: 7 additions & 0 deletions apps/mobile/drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
"when": 1786968532800,
"tag": "0000_material_toad",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1788574828397,
"tag": "0001_safe_hellfire_club",
"breakpoints": true
}
]
}
2 changes: 2 additions & 0 deletions apps/mobile/drizzle/migrations.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import journal from './meta/_journal.json';
import m0000 from './0000_material_toad.sql';
import m0001 from './0001_safe_hellfire_club.sql';

export default {
journal,
migrations: {
m0000,
m0001,
},
};
2 changes: 2 additions & 0 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@kilocode/app-shared": "workspace:*",
"@kilocode/cloud-agent-sdk": "workspace:*",
"@kilocode/event-service": "workspace:*",
"@kilocode/harness-sdk": "workspace:*",
"@kilocode/kilo-chat": "workspace:*",
"@kilocode/kilo-chat-hooks": "workspace:*",
"@kilocode/notifications": "workspace:*",
Expand All @@ -57,6 +58,7 @@
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"drizzle-orm": "catalog:",
"effect": "3.22.1",
"expo": "~57.0.21",
"expo-apple-authentication": "~57.0.1",
"expo-application": "~57.0.2",
Expand Down
8 changes: 8 additions & 0 deletions apps/mobile/src/app/(app)/(tabs)/(4_chat)/[id].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { useLocalSearchParams } from 'expo-router';

import { ChatScreen } from '@/components/chat/chat-screen';

export default function ChatConversation() {
const { id } = useLocalSearchParams<{ id: string }>();
return <ChatScreen opened={id} />;
}
4 changes: 4 additions & 0 deletions apps/mobile/src/app/(app)/(tabs)/(4_chat)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Stack } from 'expo-router';

export const unstable_settings = {
initialRouteName: 'index',
};

export default function ChatLayout() {
return <Stack screenOptions={{ headerShown: false }} />;
}
6 changes: 3 additions & 3 deletions apps/mobile/src/app/(app)/(tabs)/(4_chat)/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { QuickChatScreen } from '@/components/quick-chat/quick-chat-screen';
import { ChatListScreen } from '@/components/chat/chat-list-screen';

export default function QuickChatIndex() {
return <QuickChatScreen />;
export default function ChatIndex() {
return <ChatListScreen />;
}
18 changes: 9 additions & 9 deletions apps/mobile/src/app/(app)/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
import { StateSurfaceInsets } from '@/components/centered-state-surface';
import { BlurBar } from '@/components/ui/blur-bar';
import { Text } from '@/components/ui/text';
import { FEATURE_FLAG_QUICK_CHAT, useFeatureFlag } from '@/lib/analytics/posthog';
import { FEATURE_FLAG_CHAT, useFeatureFlag } from '@/lib/analytics/posthog';
import { PROFILE_TAB_ROOT } from '@/lib/finding-detail-back';
import { useLiveAgentSessions } from '@/lib/hooks/use-agent-sessions';
import { useKiloClawTabVisible } from '@/lib/hooks/use-kiloclaw-tab-visible';
Expand Down Expand Up @@ -72,7 +72,7 @@ export default function TabsLayout() {
const colors = useThemeColors();
const { bottom, left, right } = useSafeAreaInsets();
const { fontScale } = useWindowDimensions();
const hideTabs = shouldHideTabBar(pathname);
const hideTabs = shouldHideTabBar(pathname, segments);
const showTabLabel = shouldShowTabLabel(fontScale);
const tabBarHeight = getEffectiveTabBarHeight({
bottomInset: bottom,
Expand All @@ -82,9 +82,9 @@ export default function TabsLayout() {
const tabBarHorizontalInset = getTabBarHorizontalInset({ left, right });
const tabIconSize = getTabBarIconSize(fontScale);
const showKiloClawTab = useKiloClawTabVisible();
const showQuickChatTab = useFeatureFlag(FEATURE_FLAG_QUICK_CHAT, false);
const tabFlags = { showKiloClaw: showKiloClawTab, showQuickChat: showQuickChatTab };
const tabCount = visibleTabCount(showKiloClawTab, showQuickChatTab);
const showChatTab = useFeatureFlag(FEATURE_FLAG_CHAT, true);
const tabFlags = { showKiloClaw: showKiloClawTab, showChat: showChatTab };
const tabCount = visibleTabCount(showKiloClawTab, showChatTab);
const { t } = useTranslation();
const { organizationId, isLoaded: orgLoaded } = useOrganization();
const { activeSessions, isLoading, isError } = useLiveAgentSessions({
Expand All @@ -111,13 +111,13 @@ export default function TabsLayout() {
orgLoaded && !isLoading && !isError && needsInputCount > 0 ? needsInputCount : undefined;

// If the flag flips off while the Chat tab is focused, its `href` becomes
// null but the route is still mounted — move to Home instead.
// null but the route is still mounted, so move to Home instead.
const onChatTab = segments.some(segment => segment === '(4_chat)');
useEffect(() => {
if (!showQuickChatTab && onChatTab) {
if (!showChatTab && onChatTab) {
router.replace('/(app)/(tabs)/(0_home)' as Href);
}
}, [showQuickChatTab, onChatTab, router]);
}, [showChatTab, onChatTab, router]);

return (
<StateSurfaceInsets bottomInset={hideTabs ? 0 : tabBarHeight + 16}>
Expand Down Expand Up @@ -221,7 +221,7 @@ export default function TabsLayout() {
<Tabs.Screen
name="(4_chat)"
options={{
href: showQuickChatTab ? undefined : null,
href: showChatTab ? undefined : null,
title: t('common.chat'),
tabBarAccessibilityLabel: tabAccessibilityLabel(
t('common.chat'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ vi.mock('@/lib/hooks/use-theme-colors', () => ({
useThemeColors: () => ({ foreground: '#000000', mutedForeground: '#666666' }),
}));
vi.mock('@/lib/analytics/posthog', () => ({
FEATURE_FLAG_QUICK_CHAT: 'quick-chat',
FEATURE_FLAG_CHAT: 'mobile-chat',
useFeatureFlag: () => false,
}));
vi.mock('@/lib/hooks/use-kiloclaw-tab-visible', () => ({ useKiloClawTabVisible: () => false }));
Expand Down
16 changes: 16 additions & 0 deletions apps/mobile/src/components/chat/beta-pill.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { View } from 'react-native';
import { useTranslation } from 'react-i18next';

import { Text } from '@/components/ui/text';

/** Says, everywhere the chat is shown, that the chat is not finished yet. */
export function BetaPill() {
const { t } = useTranslation();
return (
<View className="rounded-full border border-border bg-secondary px-2 py-0.5">
<Text className="font-mono-medium text-[10px] uppercase leading-4 tracking-[0.2px] text-muted-foreground">
{t('modelChat.beta')}
</Text>
</View>
);
}
Loading