Skip to content

Commit 02b97e5

Browse files
feat(mobile): manage scheduled tasks across environments (#12351)
1 parent 9f4238f commit 02b97e5

33 files changed

Lines changed: 3839 additions & 726 deletions

‎apps/mobile/src/Stack.tsx‎

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@ import {
6868
SettingsEnvironmentNewThreadsRouteScreen,
6969
SettingsEnvironmentSourceControlRouteScreen,
7070
} from "./features/settings/SettingsServerControlsRouteScreen";
71+
import {
72+
SettingsScheduledTasksRouteScreen,
73+
SettingsScheduledTaskNewRouteScreen,
74+
SettingsScheduledTaskEditRouteScreen,
75+
} from "./features/settings/SettingsScheduledTasksRouteScreen";
76+
import {
77+
ScheduledTaskModelPickerRouteScreen,
78+
ScheduledTaskBranchPickerRouteScreen,
79+
} from "./features/settings/ScheduledTaskPickerScreens";
80+
import { ScheduledTaskEditorProvider } from "./features/settings/scheduled-task-editor";
7181
import { SettingsKeyboardRouteScreen } from "./features/settings/SettingsKeyboardRouteScreen";
7282
import { SettingsLegalRouteScreen } from "./features/settings/SettingsLegalRouteScreen";
7383
import {
@@ -271,6 +281,41 @@ const SettingsContentStack = createNativeStackNavigator({
271281
title: "Follow-ups",
272282
},
273283
}),
284+
SettingsScheduledTasks: createNativeStackScreen({
285+
screen: SettingsScheduledTasksRouteScreen,
286+
linking: "scheduled-tasks",
287+
options: {
288+
title: "Scheduled Tasks",
289+
// Leave room to center UIKit's title beside the two trailing actions.
290+
headerTitleStyle: { fontSize: 16, fontWeight: "800" },
291+
},
292+
}),
293+
SettingsScheduledTaskNew: createNativeStackScreen({
294+
screen: SettingsScheduledTaskNewRouteScreen,
295+
linking: "scheduled-tasks/new",
296+
options: { title: "New scheduled task" },
297+
}),
298+
SettingsScheduledTaskEdit: createNativeStackScreen({
299+
screen: SettingsScheduledTaskEditRouteScreen,
300+
options: { title: "Edit scheduled task" },
301+
}),
302+
SettingsScheduledTaskBranch: createNativeStackScreen({
303+
screen: ScheduledTaskBranchPickerRouteScreen,
304+
options: { title: "Base branch" },
305+
}),
306+
SettingsScheduledTaskModel: createNativeStackScreen({
307+
screen: ScheduledTaskModelPickerRouteScreen,
308+
options: {
309+
headerShown: false,
310+
...(Platform.OS === "android"
311+
? { presentation: "card" as const }
312+
: {
313+
...FORM_SHEET_PRESENTATION_OPTIONS,
314+
sheetAllowedDetents: [1],
315+
sheetGrabberVisible: true,
316+
}),
317+
},
318+
}),
274319
SettingsClientStorage: createNativeStackScreen({
275320
screen: SettingsClientStorageRouteScreen,
276321
linking: "client-storage",
@@ -326,7 +371,9 @@ const SettingsSheetStack = createNativeStackNavigator({
326371
screen: SettingsContentStack,
327372
linking: "",
328373
layout: ({ children }) => (
329-
<SettingsEnvironmentFilterProvider>{children}</SettingsEnvironmentFilterProvider>
374+
<SettingsEnvironmentFilterProvider>
375+
<ScheduledTaskEditorProvider>{children}</ScheduledTaskEditorProvider>
376+
</SettingsEnvironmentFilterProvider>
330377
),
331378
}),
332379
SettingsAuth: createNativeStackScreen({

‎apps/mobile/src/components/EnvironmentMachineSymbol.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import type { EnvironmentMachineKind } from "@t3tools/contracts";
22
import { SymbolView, type AppSymbolName } from "./AppSymbol";
33

4-
const SYMBOL_BY_KIND: Record<EnvironmentMachineKind, AppSymbolName> = {
4+
export const ENVIRONMENT_MACHINE_SYMBOLS = {
55
server: "server.rack",
66
cloud: "cloud",
77
linux: "terminal",
88
desktop: "desktopcomputer",
99
laptop: "laptopcomputer",
1010
"mac-mini": "macmini",
1111
"mac-studio": "macstudio",
12-
};
12+
} as const satisfies Record<EnvironmentMachineKind, AppSymbolName>;
1313

1414
export const ENVIRONMENT_MACHINE_KIND_LABELS: Record<EnvironmentMachineKind, string> = {
1515
server: "Server",
@@ -30,7 +30,7 @@ export function EnvironmentMachineSymbol(props: {
3030
return (
3131
<SymbolView
3232
accessibilityLabel={ENVIRONMENT_MACHINE_KIND_LABELS[props.kind]}
33-
name={SYMBOL_BY_KIND[props.kind]}
33+
name={ENVIRONMENT_MACHINE_SYMBOLS[props.kind]}
3434
size={props.size}
3535
tintColorClassName={props.tintColorClassName}
3636
type="monochrome"

‎apps/mobile/src/features/keyboard/CommandPalette.tsx‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,17 @@ export function CommandPalette(props: {
204204
params: { screen: "SettingsEnvironments" },
205205
}),
206206
},
207+
{
208+
key: "scheduledTasks",
209+
kind: "action",
210+
title: "Scheduled tasks",
211+
searchTerms: ["schedule", "automations", "recurring"],
212+
run: () =>
213+
navigation.navigate("SettingsSheet", {
214+
screen: "SettingsContent",
215+
params: { screen: "SettingsScheduledTasks" },
216+
}),
217+
},
207218
{
208219
key: "usage",
209220
kind: "action",
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { useNavigation } from "@react-navigation/native";
2+
import * as Haptics from "expo-haptics";
3+
import { useMemo, useState } from "react";
4+
5+
import { AppText as Text } from "../../components/AppText";
6+
import { buildModelOptions, groupByProvider } from "../../lib/modelOptions";
7+
import { resolveProviderOptionDescriptors } from "../../lib/providerOptions";
8+
import { useEnvironmentServerConfig, useProjects } from "../../state/entities";
9+
import { useDebouncedValue, usePaginatedBranches } from "../../state/queries";
10+
import { BranchPickerScreen } from "../threads/NewTaskContextPickerScreens";
11+
import { ThreadSettingsPickerScreen } from "../threads/ThreadSettingsSheet";
12+
import { SettingsScreen } from "./components/SettingsScreen";
13+
import { useScheduledTaskEditor } from "./scheduled-task-editor";
14+
15+
function MissingTaskDraft() {
16+
return (
17+
<SettingsScreen title="Scheduled task">
18+
<Text className="p-5 text-base text-foreground-muted">Open a scheduled task form first.</Text>
19+
</SettingsScreen>
20+
);
21+
}
22+
23+
export function ScheduledTaskModelPickerRouteScreen() {
24+
const navigation = useNavigation();
25+
const { editor, setEditor } = useScheduledTaskEditor();
26+
const config = useEnvironmentServerConfig(editor?.environmentId ?? null);
27+
const selectedModel = editor?.draft.modelSelection ?? null;
28+
const models = useMemo(() => buildModelOptions(config, selectedModel), [config, selectedModel]);
29+
const providerGroups = useMemo(() => groupByProvider(models), [models]);
30+
const selectedOption = models.find(
31+
(option) =>
32+
option.selection.instanceId === selectedModel?.instanceId &&
33+
option.selection.model === selectedModel.model,
34+
);
35+
const optionDescriptors = useMemo(
36+
() =>
37+
resolveProviderOptionDescriptors({
38+
capabilities: selectedOption?.capabilities,
39+
selections: selectedModel?.options,
40+
}),
41+
[selectedOption?.capabilities, selectedModel?.options],
42+
);
43+
44+
if (!editor) return <MissingTaskDraft />;
45+
46+
return (
47+
<ThreadSettingsPickerScreen
48+
environmentId={editor.environmentId}
49+
providerGroups={providerGroups}
50+
selectedModel={selectedModel}
51+
onSelectModel={(option) =>
52+
setEditor((current) =>
53+
current
54+
? {
55+
...current,
56+
draft: {
57+
...current.draft,
58+
modelSelection: option.selection,
59+
modelSelectionIsExplicit: true,
60+
},
61+
}
62+
: current,
63+
)
64+
}
65+
optionDescriptors={optionDescriptors}
66+
onUpdateOptionSelections={(options) =>
67+
setEditor((current) =>
68+
current?.draft.modelSelection
69+
? {
70+
...current,
71+
draft: {
72+
...current.draft,
73+
modelSelection: { ...current.draft.modelSelection, options },
74+
modelSelectionIsExplicit: true,
75+
},
76+
}
77+
: current,
78+
)
79+
}
80+
runtimeMode={editor.draft.runtimeMode}
81+
onUpdateRuntimeMode={(runtimeMode) =>
82+
setEditor((current) =>
83+
current
84+
? {
85+
...current,
86+
draft: { ...current.draft, runtimeMode },
87+
}
88+
: current,
89+
)
90+
}
91+
onClose={() => navigation.goBack()}
92+
/>
93+
);
94+
}
95+
96+
export function ScheduledTaskBranchPickerRouteScreen() {
97+
const navigation = useNavigation();
98+
const { editor, setEditor } = useScheduledTaskEditor();
99+
const projects = useProjects();
100+
const project =
101+
projects.find(
102+
(entry) =>
103+
entry.environmentId === editor?.environmentId && entry.id === editor.draft.projectId,
104+
) ?? null;
105+
const [query, setQuery] = useState("");
106+
const debouncedQuery = useDebouncedValue(query, 150);
107+
const branches = usePaginatedBranches({
108+
environmentId: editor?.environmentId ?? null,
109+
cwd: project?.workspaceRoot ?? null,
110+
query: debouncedQuery,
111+
});
112+
const visibleBranches = branches.refs.filter(
113+
(branch) => !branch.isRemote && branch.name.toLowerCase().includes(query.trim().toLowerCase()),
114+
);
115+
116+
if (!editor) return <MissingTaskDraft />;
117+
118+
return (
119+
<BranchPickerScreen
120+
title="Base branch"
121+
project={project}
122+
branches={visibleBranches}
123+
selectedBranchName={editor.draft.baseRef}
124+
query={query}
125+
onQueryChange={setQuery}
126+
loading={
127+
query.trim() !== debouncedQuery.trim() || (branches.isPending && branches.data === null)
128+
}
129+
error={project ? branches.error : "This project is no longer available."}
130+
refreshing={branches.isFetchingNextPage}
131+
hasMore={branches.data?.nextCursor != null}
132+
onRefresh={branches.refresh}
133+
onLoadMore={branches.loadNext}
134+
onSelect={(branch) => {
135+
void Haptics.selectionAsync();
136+
setEditor((current) =>
137+
current ? { ...current, draft: { ...current.draft, baseRef: branch.name } } : current,
138+
);
139+
navigation.goBack();
140+
}}
141+
worktree={{
142+
startFromOrigin: editor.draft.startFromOrigin,
143+
onChangeStartFromOrigin: (startFromOrigin) =>
144+
setEditor((current) =>
145+
current
146+
? {
147+
...current,
148+
draft: { ...current.draft, startFromOrigin },
149+
}
150+
: current,
151+
),
152+
}}
153+
/>
154+
);
155+
}

‎apps/mobile/src/features/settings/SettingsRouteScreen.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ function SettingsIndexSections() {
142142
) : null}
143143
</SettingsSection>
144144

145+
<SettingsSection title="Automations">
146+
<SettingsRow icon="clock" label="Scheduled tasks" target="SettingsScheduledTasks" />
147+
</SettingsSection>
148+
145149
<SettingsSection title="Projects & threads">
146150
{selectedProjectKey !== null ? (
147151
<SettingsRow

0 commit comments

Comments
 (0)