Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
5ba9515
feat(server): establish Kilo SDK session boundary
thomasbrugman Oct 3, 2026
dfa6812
ci: verify Kilo sessions on the fork Linux runner
thomasbrugman Oct 3, 2026
b0c086d
feat(provider): integrate isolated local Kilo sessions
thomasbrugman Oct 3, 2026
98ad7f5
ci: identify the Kilo fork by repository ID
thomasbrugman Oct 3, 2026
3a705c7
ci: install Linux desktop build dependencies
thomasbrugman Oct 3, 2026
c406f96
test(provider): verify all Kilo text generation operations
thomasbrugman Oct 3, 2026
fee1809
feat(provider): connect isolated Kilo Cloud tasks to orchestration
thomasbrugman Oct 3, 2026
7704989
fix(web): keep cloud composer separate from local checkout controls
thomasbrugman Oct 3, 2026
3bedf8c
chore: remove temporary Kilo contribution workflow
thomasbrugman Oct 3, 2026
3edb86e
fix(kilo): retire local sessions when profile credentials change
thomasbrugman Oct 3, 2026
ee9f665
fix(kilo): block external plugins before native session startup
thomasbrugman Oct 3, 2026
16a4ff0
fix(kilo): refuse unsafe local runtime and isolate cloud actions
thomasbrugman Oct 3, 2026
e5c0af3
test(kilo): use typed event listener in account fixture
thomasbrugman Oct 3, 2026
86b96be
fix(kilo): refresh mobile workspace gate and verify blocked UI
thomasbrugman Oct 3, 2026
74d0a46
fix(kilo): trust native configuration and recover cloud results
thomasbrugman Oct 3, 2026
53e8d89
fix(kilo): identify result failures and keep fixtures offline
thomasbrugman Oct 3, 2026
4d7e7cf
Merge pull request #3 from thomasbrugman/feat/kilo-provider-v2
thomasbrugman Oct 3, 2026
c937f1c
fix(kilo): recover unsent cloud requests without retrying uncertain s…
thomasbrugman Oct 4, 2026
179daef
fix(kilo): preserve recovery bounds and local workspaces
thomasbrugman Oct 4, 2026
d7db243
test(kilo): encode recovery fixture with its journal schema
thomasbrugman Oct 4, 2026
1baf9ad
fix(kilo): preserve cloud outcomes across storage failures
thomasbrugman Oct 4, 2026
894f31b
fix(kilo): observe a concurrently persisted cloud stop
thomasbrugman Oct 4, 2026
1fb6d4d
fix(kilo): keep recovery and cleanup observable during failures
thomasbrugman Oct 4, 2026
2aeba5a
test(kilo): release delayed saves when assertions fail
thomasbrugman Oct 4, 2026
feaca3d
Merge pull request #4 from thomasbrugman/fix/kilo-admission-review-20…
thomasbrugman Oct 4, 2026
c7c85dd
merge: integrate upstream main with Kilo provider lifecycle
thomasbrugman Oct 4, 2026
1efb73b
fix(kilo): require an explicit repository for live evidence
thomasbrugman Oct 4, 2026
8964a35
fix(web): hide local project settings in cloud threads
thomasbrugman Oct 4, 2026
c36c134
fix(kilo): verify observed process exit before replacement
thomasbrugman Oct 4, 2026
ff33edb
merge: sync Kilo integration with upstream 06e627448
thomasbrugman Oct 5, 2026
567b771
fix(kilo): keep explicit MCP cloud targets off local workspaces
thomasbrugman Oct 5, 2026
2fb1371
merge: complete Kilo upstream sync at 250e052f4
thomasbrugman Oct 5, 2026
2df10b2
Merge pull request #5 from thomasbrugman/integrate/kilo-upstream-2026…
thomasbrugman Oct 5, 2026
f53552b
Merge remote-tracking branch 'upstream/main' into base/kilo-provider-…
claude Oct 5, 2026
74f003d
refactor(kilo): trim the provider to the code it runs
claude Oct 5, 2026
43876cc
refactor(web): guard the cloud right panel where it renders
claude Oct 5, 2026
dab8a11
refactor(kilo): share part nodes, request settlement and turn closing
claude Oct 5, 2026
d3649ac
test(kilo): share Kilo Cloud fixture setup across cases
claude Oct 5, 2026
5050de8
refactor(orchestration): read remote execution from capabilities
claude Oct 5, 2026
062465c
test(kilo): compact the live adapter fixture
claude Oct 5, 2026
bfd9d45
refactor(orchestration): keep upstream's start-failure chain intact
claude Oct 5, 2026
190ebf2
test(kilo): check foreign permission and question replies in one case
claude Oct 5, 2026
fa50b96
fix(kilo): recover cloud runs without resubmitting tasks
thomasbrugman Oct 5, 2026
0c54fdb
fix(web): keep cloud sends clear of local workspace actions
thomasbrugman Oct 5, 2026
51008c2
fix(web): isolate cloud targets from local workspace UI
thomasbrugman Oct 5, 2026
57af337
fix(web): drop saved worktree from cloud draft bootstrap
thomasbrugman Oct 5, 2026
6bb1f9b
fix(web): preserve native close shortcut in cloud threads
thomasbrugman Oct 5, 2026
3f42180
fix(kilo): reconcile upstream service and Effect APIs
thomasbrugman Oct 6, 2026
2acd6ec
fix(kilo): keep session and cloud recovery live
thomasbrugman Oct 6, 2026
43fce22
fix(mobile): gate Kilo terminal routes on workspace availability
thomasbrugman Oct 6, 2026
3f1adfd
fix(kilo): preserve cloud recovery across upstream Stop changes
thomasbrugman Oct 6, 2026
75f941c
fix(kilo): address workspace actions and service review findings
thomasbrugman Oct 6, 2026
8087e68
fix(kilo): preserve cloud guards with server browser previews
thomasbrugman Oct 6, 2026
ef21baa
fix(kilo): download cloud file previews
thomasbrugman Oct 6, 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
44 changes: 44 additions & 0 deletions apps/mobile/src/components/LocalWorkspaceNotice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { NativeStackScreenOptions } from "../native/StackHeader";
import { ActivityIndicator, View } from "react-native";
import { EmptyState } from "./EmptyState";
import { AppText } from "./AppText";
import type { threadLocalWorkspace } from "../state/threadLocalWorkspace";

export function LocalWorkspaceNotice({
state,
title,
}: {
readonly title: string;
readonly state: ReturnType<typeof threadLocalWorkspace>["localWorkspaceState"];
}) {
return (
<View className="flex-1 items-center justify-center bg-sheet px-6">
<NativeStackScreenOptions options={{ title }} />
{state === "loading" ? (
<View className="items-center gap-4">
<ActivityIndicator />
<AppText className="text-sm text-foreground-muted">
Loading conversation workspace...
</AppText>
</View>
) : (
<EmptyState
title={
state === "error"
? "Workspace could not be loaded"
: state === "cloud"
? "Remote workspace"
: "Workspace unavailable"
}
detail={
state === "error"
? "Check the environment connection and reopen this conversation to try again."
: state === "cloud"
? "This cloud conversation cannot use local files, Git, terminals or review diffs."
: "This conversation has no available local workspace."
}
/>
)}
</View>
);
}
10 changes: 10 additions & 0 deletions apps/mobile/src/components/ProviderIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { kiloIconPaths } from "@t3tools/client-runtime/kilo-icon";
import { Image } from "expo-image";
import { Path, Svg } from "react-native-svg";
import { View } from "react-native";
Expand Down Expand Up @@ -133,6 +134,15 @@ export function ProviderIcon(props: ProviderIconProps) {
);
}

if (props.provider === "kilo" || props.provider === "kilo-cloud") {
return (
<Svg width={size} height={size} viewBox="0 0 512 512">
{kiloIconPaths.map((path) => (
<Path key={path.d} {...path} />
))}
</Svg>
);
}
if (props.provider === "opencode") {
return (
<Svg width={size} height={size} viewBox="0 0 32 40" fill="none">
Expand Down
44 changes: 22 additions & 22 deletions apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { LocalWorkspaceNotice } from "../../components/LocalWorkspaceNotice";
import { NativeStackScreenOptions } from "../../native/StackHeader";
import { StackActions, useNavigation, type StaticScreenProps } from "@react-navigation/native";
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
Expand Down Expand Up @@ -350,7 +351,8 @@ function useThreadFilesWorkspace(params: {
const routeCwd = firstRouteParam(params.cwd);
const routeProjectName = firstRouteParam(params.projectName);
const { selectedThread, selectedThreadProject } = useThreadSelection();
const { selectedThreadCwd } = useSelectedThreadWorktree();
const { selectedThreadCwd, localWorkspaceEnabled, localWorkspaceState } =
useSelectedThreadWorktree();
const environmentId =
routeEnvironmentId !== null
? EnvironmentId.make(routeEnvironmentId)
Expand All @@ -362,26 +364,20 @@ function useThreadFilesWorkspace(params: {
} | null;

return {
cwd: routeCwd ?? selectedThreadCwd ?? project?.workspaceRoot ?? null,
localWorkspaceState: routeThreadId !== null ? localWorkspaceState : ("unavailable" as const),
cwd:
routeThreadId !== null
? localWorkspaceEnabled
? selectedThreadCwd
: null
: (routeCwd ?? project?.workspaceRoot ?? null),
environmentId,
projectName: routeProjectName ?? project?.title ?? "Files",
selectedThread,
threadId,
};
}

function FilesUnavailable() {
return (
<View className="flex-1 items-center justify-center bg-sheet px-6">
<NativeStackScreenOptions options={{ title: "Files" }} />
<EmptyState
title="Files unavailable"
detail="This thread does not have an active workspace path."
/>
</View>
);
}

function FilesToolbarBottomFade() {
const sheetColor = String(useUniwindTheme()["--color-sheet"]);

Expand Down Expand Up @@ -416,9 +412,8 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
const { fileInspector, layout, showAuxiliaryPane } = useAdaptiveWorkspaceLayout();
const [searchQuery, setSearchQuery] = useState("");
const { themeAppearance: highlightTheme } = useAppearancePreferences();
const { cwd, environmentId, projectName, selectedThread, threadId } = useThreadFilesWorkspace(
props.route.params,
);
const { cwd, environmentId, projectName, selectedThread, threadId, localWorkspaceState } =
useThreadFilesWorkspace(props.route.params);
const revealedInspectorRef = useRef(false);
const entriesQuery = useFileTreeEntries({
environmentId,
Expand Down Expand Up @@ -496,6 +491,9 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
}
}, [cwd, fileInspector.supported, showAuxiliaryPane]);

if (threadId !== null && localWorkspaceState !== "local" && localWorkspaceState !== "loading")
return <LocalWorkspaceNotice title="Files" state={localWorkspaceState} />;

if (selectedThread === null || environmentId === null || threadId === null) {
if (fileInspector.supported) {
return (
Expand All @@ -510,7 +508,7 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
}

if (cwd === null) {
return <FilesUnavailable />;
return <LocalWorkspaceNotice title="Files" state={localWorkspaceState} />;
}

if (fileInspector.supported) {
Expand Down Expand Up @@ -564,9 +562,8 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) {
const params = props.route.params;
const relativePath = normalizeRoutePath(params.path);
const targetLine = normalizeRouteLine(firstRouteParam(params.line));
const { cwd, environmentId, projectName, selectedThread, threadId } = useThreadFilesWorkspace(
props.route.params,
);
const { cwd, environmentId, projectName, selectedThread, threadId, localWorkspaceState } =
useThreadFilesWorkspace(props.route.params);
const [modeOverride, setModeOverride] = useState<{
readonly path: string;
readonly mode: FileViewMode;
Expand Down Expand Up @@ -854,6 +851,9 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) {
handleReturnToThread();
}, [handleReturnToThread, navigation]);

if (threadId !== null && localWorkspaceState !== "local" && localWorkspaceState !== "loading")
return <LocalWorkspaceNotice title="Files" state={localWorkspaceState} />;

// A file opened from a project draft has no thread, and needs none: the thread only supplies
// the workspace to read from and the target to navigate back to, both of which a draft names
// for itself. Wait only for what this file actually cannot render without.
Expand All @@ -862,7 +862,7 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) {
}

if (cwd === null) {
return <FilesUnavailable />;
return <LocalWorkspaceNotice title="Files" state={localWorkspaceState} />;
}

if (relativePath === null) {
Expand Down
10 changes: 9 additions & 1 deletion apps/mobile/src/features/review/ReviewSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { LocalWorkspaceNotice } from "../../components/LocalWorkspaceNotice";
import type { EnvironmentId, ThreadId } from "@t3tools/contracts";
import { useNavigation, type StaticScreenProps } from "@react-navigation/native";
import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader";
Expand Down Expand Up @@ -447,8 +448,15 @@ type ReviewSheetProps = StaticScreenProps<{
}>;

export function ReviewSheet(props: ReviewSheetProps) {
const { nativeReviewDiffStyle } = useAppearanceCodeSurface();
useAdaptiveWorkspacePaneRole("inspector");
const { localWorkspaceEnabled, localWorkspaceState } = useSelectedThreadWorktree();
if (!localWorkspaceEnabled)
return <LocalWorkspaceNotice title="Review changes" state={localWorkspaceState} />;
return <LocalReviewSheet {...props} />;
}

function LocalReviewSheet(props: ReviewSheetProps) {
const { nativeReviewDiffStyle } = useAppearanceCodeSurface();
const { panes, showAuxiliaryPane } = useAdaptiveWorkspaceLayout();
const navigation = useNavigation();
const insets = useSafeAreaInsets();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export function useReviewHeaderPresentation(props: {
const gitMenuAvailable =
selectedThread !== null && String(selectedThread.id) === String(props.threadId);
const gitMenu = useThreadGitMenuDefinition({
gitControlsEnabled: props.selectedThreadCwd !== null,
environmentId: props.environmentId,
threadId: props.threadId,
currentBranch: selectedThread?.branch ?? null,
Expand Down
3 changes: 2 additions & 1 deletion apps/mobile/src/features/review/useReviewSections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ export function useReviewSections(input: {
readonly reviewCache: ReviewCacheForThread;
}) {
const { environmentId, reviewCache, threadId } = input;
const enabled = input.enabled ?? true;
const { localWorkspaceEnabled } = useSelectedThreadWorktree();
const enabled = localWorkspaceEnabled && (input.enabled ?? true);
const selectedThread = useSelectedThreadProjection();
const { selectedThreadCwd } = useSelectedThreadWorktree();
const diffPreview = useEnvironmentQuery(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useRef, useState } from "react";
import { Alert, Linking, Pressable, ScrollView, TextInput, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";

import { ProviderIcon } from "../../components/ProviderIcon";
import { AppText as Text } from "../../components/AppText";
import { ScreenScrollView } from "../../components/ScreenScrollView";
import { useEnvironmentQuery } from "../../state/query";
Expand Down Expand Up @@ -43,18 +44,42 @@ export function SettingsProviderAccountsRouteScreen() {
{environment.serverConfig.providers
.filter(
(provider) =>
provider.driver === "kilo-cloud" ||
provider.driver === "kilo" ||
provider.setup?.canAuthenticate ||
(provider.driver === "acpRegistry" && provider.installed),
)
.map((provider) => (
<ProviderAccount
key={provider.instanceId}
environment={environment}
provider={provider}
/>
))}
.map((provider) =>
provider.driver === "kilo" || provider.driver === "kilo-cloud" ? (
<View
key={provider.instanceId}
className="gap-2 border-b border-border-subtle p-4"
>
<View className="flex-row items-center gap-2">
<ProviderIcon provider="kilo" size={24} />
<Text>{provider.displayName}</Text>
</View>
{provider.message ? (
<Text className="text-foreground-muted">{provider.message}</Text>
) : null}
<Text className="text-foreground-muted">
{provider.driver === "kilo-cloud"
? "Kilo Cloud runs in a remote repository and uses Kilo credit. Closing T3 does not stop remote work or billing. Manage its account and repository in web or desktop Settings."
: "Kilo runs on this environment and trusts native configuration, plugins and MCP servers. Tool approvals are not a sandbox. Manage its account profile in web or desktop Settings."}
</Text>
</View>
) : (
<ProviderAccount
key={provider.instanceId}
environment={environment}
provider={provider}
/>
),
)}
{!environment.serverConfig.providers.some(
(provider) =>
provider.driver === "kilo-cloud" ||
provider.driver === "kilo" ||
provider.setup?.canAuthenticate ||
(provider.driver === "acpRegistry" && provider.installed),
) ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { LocalWorkspaceNotice } from "../../components/LocalWorkspaceNotice";
import { useSelectedThreadWorktree } from "../../state/use-selected-thread-worktree";
import { DEFAULT_TERMINAL_ID, EnvironmentId, ThreadId } from "@t3tools/contracts";
import { type KnownTerminalSession } from "@t3tools/client-runtime/state/terminal";
import { SymbolView } from "../../components/AppSymbol";
Expand Down Expand Up @@ -239,6 +241,13 @@ type ThreadTerminalRouteScreenProps = StaticScreenProps<{
}>;

export function ThreadTerminalRouteScreen(props: ThreadTerminalRouteScreenProps) {
const { localWorkspaceEnabled, localWorkspaceState } = useSelectedThreadWorktree();
if (!localWorkspaceEnabled)
return <LocalWorkspaceNotice title="Terminal" state={localWorkspaceState} />;
return <LocalThreadTerminalRouteScreen {...props} />;
}

function LocalThreadTerminalRouteScreen(props: ThreadTerminalRouteScreenProps) {
const insets = useSafeAreaInsets();
const navigation = useNavigation();
const writeTerminal = useAtomCommand(terminalEnvironment.write, "terminal write");
Expand Down
46 changes: 42 additions & 4 deletions apps/mobile/src/features/threads/ThreadDetailScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useAtomValue } from "@effect/atom-react";
import { useThreadReportedModelSelection } from "../../state/entities";
import { cloudExecutionLabel } from "@t3tools/client-runtime/cloudExecutionLabels";
import { AppText } from "../../components/AppText";
import { useThreadCloudExecution, useThreadReportedModelSelection } from "../../state/entities";
import { UsageLimitRecoveryCard } from "./UsageLimitRecoveryCard";
import { useNavigation } from "@react-navigation/native";
import type { WorktreeSetupCardProps } from "./worktree-setup-card";
Expand Down Expand Up @@ -316,6 +318,10 @@ const USER_INPUT_TOGGLE_TIMING = {

export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: ThreadDetailScreenProps) {
const navigation = useNavigation();
const cloudExecution = useThreadCloudExecution({
environmentId: props.environmentId,
threadId: props.selectedThread.id,
});
const { session: voiceInputSession } = useGlobalVoiceInput();
const reportedModelSelection = useThreadReportedModelSelection({
environmentId: props.environmentId,
Expand All @@ -335,21 +341,26 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
threadId: props.selectedThread.id,
});
}, [navigation, props.environmentId, props.selectedThread.id]);
const browserPreviewAvailable =
props.threadCwd !== null &&
!cloudExecution &&
props.serverConfig?.environment.capabilities.serverBrowser === true;
const browserTabs = useThreadServerBrowserTabs({
environmentId: props.environmentId,
threadId: props.selectedThread.id,
enabled: props.serverConfig?.environment.capabilities.serverBrowser === true,
enabled: browserPreviewAvailable,
});
const openBrowserPreview = useCallback(
(tabId?: string) => {
if (!browserPreviewAvailable) return;
Keyboard.dismiss();
navigation.navigate("ThreadBrowserPreview", {
environmentId: props.environmentId,
threadId: props.selectedThread.id,
...(tabId === undefined ? {} : { tabId }),
});
},
[navigation, props.environmentId, props.selectedThread.id],
[browserPreviewAvailable, navigation, props.environmentId, props.selectedThread.id],
);
const insets = useSafeAreaInsets();
const isKeyboardVisible = useKeyboardState((state) => state.isVisible);
Expand Down Expand Up @@ -1148,7 +1159,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
<View className="flex-1" />
)}

{showContent ? (
{showContent && browserPreviewAvailable ? (
<ThreadBrowserFloat
key={selectedThreadKey}
environmentId={props.environmentId}
Expand Down Expand Up @@ -1336,6 +1347,33 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
: undefined
}
>
{cloudExecution ? (
<View className="px-3 py-2">
<AppText>
Kilo Cloud · {cloudExecution.repository} · {cloudExecution.branch}
</AppText>
<AppText>
Last observation:{" "}
{cloudExecution.observedAt
? new Date(cloudExecution.observedAt).toLocaleString()
: "unavailable"}
. Task: {cloudExecutionLabel(cloudExecution.task)}.{" "}
{cloudExecution.result
? `Result: ${cloudExecutionLabel(cloudExecution.result)}. `
: ""}
Sandbox: {cloudExecutionLabel(cloudExecution.sandbox)}. Compute:{" "}
{cloudExecutionLabel(cloudExecution.billing)}
{cloudExecution.billingAttribution === "payer_shared"
? " (shared account)"
: ""}
.
</AppText>
<AppText>
Remote repository. Closing T3 does not stop remote work or billing. Local
files and checkpoints are unavailable.
</AppText>
</View>
) : null}
{isProviderSubagent ? (
<View
className="self-center px-3 pt-1.5"
Expand Down
Loading
Loading