Skip to content

Commit 011472c

Browse files
author
Test
committed
🤖 feat: add slash commands to RN chat
_Generated with _ Change-Id: I54985c290c02f22d3e6592bfce7dad28159161f1 Signed-off-by: Test <test@example.com>
1 parent c7fe563 commit 011472c

File tree

11 files changed

+1500
-436
lines changed

11 files changed

+1500
-436
lines changed

apps/mobile/src/api/client.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@ export interface CmuxMobileClientConfig {
2424
}
2525

2626
const IPC_CHANNELS = {
27+
PROVIDERS_SET_CONFIG: "providers:setConfig",
28+
PROVIDERS_LIST: "providers:list",
2729
WORKSPACE_LIST: "workspace:list",
2830
WORKSPACE_CREATE: "workspace:create",
2931
WORKSPACE_REMOVE: "workspace:remove",
3032
WORKSPACE_RENAME: "workspace:rename",
33+
WORKSPACE_FORK: "workspace:fork",
3134
WORKSPACE_SEND_MESSAGE: "workspace:sendMessage",
3235
WORKSPACE_INTERRUPT_STREAM: "workspace:interruptStream",
36+
WORKSPACE_TRUNCATE_HISTORY: "workspace:truncateHistory",
3337
WORKSPACE_GET_INFO: "workspace:getInfo",
3438
WORKSPACE_EXECUTE_BASH: "workspace:executeBash",
3539
WORKSPACE_CHAT_PREFIX: "workspace:chat:",
@@ -158,6 +162,38 @@ export function createClient(cfg: CmuxMobileClientConfig = {}) {
158162
}
159163

160164
return {
165+
providers: {
166+
list: async (): Promise<string[]> => invoke(IPC_CHANNELS.PROVIDERS_LIST),
167+
setProviderConfig: async (
168+
provider: string,
169+
keyPath: string[],
170+
value: string
171+
): Promise<Result<void, string>> => {
172+
try {
173+
assert(typeof provider === "string" && provider.trim().length > 0, "provider required");
174+
assert(Array.isArray(keyPath) && keyPath.length > 0, "keyPath required");
175+
keyPath.forEach((segment, index) => {
176+
assert(
177+
typeof segment === "string" && segment.trim().length > 0,
178+
`keyPath segment ${index} must be a non-empty string`
179+
);
180+
});
181+
assert(typeof value === "string", "value must be a string");
182+
183+
const normalizedProvider = provider.trim();
184+
const normalizedPath = keyPath.map((segment) => segment.trim());
185+
await invoke(IPC_CHANNELS.PROVIDERS_SET_CONFIG, [
186+
normalizedProvider,
187+
normalizedPath,
188+
value,
189+
]);
190+
return { success: true, data: undefined };
191+
} catch (error) {
192+
const err = error instanceof Error ? error.message : String(error);
193+
return { success: false, error: err };
194+
}
195+
},
196+
},
161197
projects: {
162198
list: async (): Promise<ProjectsListResponse> => invoke(IPC_CHANNELS.PROJECT_LIST),
163199
listBranches: async (
@@ -219,6 +255,23 @@ export function createClient(cfg: CmuxMobileClientConfig = {}) {
219255
return { success: false, error: err };
220256
}
221257
},
258+
fork: async (
259+
workspaceId: string,
260+
newName: string
261+
): Promise<
262+
| { success: true; metadata: FrontendWorkspaceMetadata; projectPath: string }
263+
| { success: false; error: string }
264+
> => {
265+
try {
266+
assert(typeof newName === "string" && newName.trim().length > 0, "newName required");
267+
return await invoke(IPC_CHANNELS.WORKSPACE_FORK, [ensureWorkspaceId(workspaceId), newName.trim()]);
268+
} catch (error) {
269+
return {
270+
success: false,
271+
error: error instanceof Error ? error.message : String(error),
272+
};
273+
}
274+
},
222275
rename: async (
223276
workspaceId: string,
224277
newName: string
@@ -244,6 +297,22 @@ export function createClient(cfg: CmuxMobileClientConfig = {}) {
244297
return { success: false, error: err };
245298
}
246299
},
300+
truncateHistory: async (
301+
workspaceId: string,
302+
percentage = 1.0
303+
): Promise<Result<void, string>> => {
304+
try {
305+
assert(typeof percentage === "number" && Number.isFinite(percentage), "percentage must be a number");
306+
await invoke(IPC_CHANNELS.WORKSPACE_TRUNCATE_HISTORY, [
307+
ensureWorkspaceId(workspaceId),
308+
percentage,
309+
]);
310+
return { success: true, data: undefined };
311+
} catch (error) {
312+
const err = error instanceof Error ? error.message : String(error);
313+
return { success: false, error: err };
314+
}
315+
},
247316
replaceChatHistory: async (
248317
workspaceId: string,
249318
summaryMessage: {
@@ -470,3 +539,5 @@ export function createClient(cfg: CmuxMobileClientConfig = {}) {
470539
},
471540
} as const;
472541
}
542+
543+
export type CmuxMobileClient = ReturnType<typeof createClient>;

apps/mobile/src/components/RunSettingsSheet.tsx

Lines changed: 70 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export function RunSettingsSheet(props: RunSettingsSheetProps): JSX.Element {
7878
<View style={[styles.container, { backgroundColor: theme.colors.background }]}>
7979
<View style={styles.header}>
8080
<ThemedText variant="titleMedium" weight="semibold" style={styles.headerTitle}>
81-
Run settings
81+
Settings
8282
</ThemedText>
8383
<Pressable onPress={props.onClose} style={styles.closeButton}>
8484
<Ionicons name="close" size={20} color={theme.colors.foregroundPrimary} />
@@ -161,12 +161,7 @@ export function RunSettingsSheet(props: RunSettingsSheetProps): JSX.Element {
161161
</View>
162162
)}
163163

164-
<ScrollView
165-
style={styles.modelList}
166-
nestedScrollEnabled
167-
keyboardShouldPersistTaps="handled"
168-
showsVerticalScrollIndicator
169-
>
164+
<View style={styles.modelList}>
170165
{filteredModels.length === 0 ? (
171166
<View style={{ padding: 24 }}>
172167
<ThemedText variant="caption" style={{ textAlign: "center" }}>
@@ -205,7 +200,43 @@ export function RunSettingsSheet(props: RunSettingsSheetProps): JSX.Element {
205200
</View>
206201
))
207202
)}
208-
</ScrollView>
203+
</View>
204+
{props.supportsExtendedContext ? (
205+
<View
206+
style={{
207+
marginTop: 20,
208+
paddingTop: 12,
209+
borderTopWidth: StyleSheet.hairlineWidth,
210+
borderBottomWidth: StyleSheet.hairlineWidth,
211+
borderTopColor: theme.colors.border,
212+
borderBottomColor: theme.colors.border,
213+
paddingBottom: 12,
214+
gap: 8,
215+
}}
216+
>
217+
<View style={styles.sectionHeader}>
218+
<ThemedText weight="semibold">Context window</ThemedText>
219+
<Ionicons
220+
name="information-circle-outline"
221+
size={16}
222+
color={theme.colors.foregroundMuted}
223+
/>
224+
</View>
225+
<ThemedText variant="caption" style={{ color: theme.colors.foregroundMuted }}>
226+
Use the 1M-token Anthropic context window when supported.
227+
</ThemedText>
228+
<View style={styles.toggleRow}>
229+
<ThemedText weight="semibold">1M token context</ThemedText>
230+
<Switch
231+
trackColor={{ false: theme.colors.inputBorder, true: theme.colors.accent }}
232+
thumbColor={props.use1MContext ? theme.colors.accent : theme.colors.surface}
233+
value={props.use1MContext}
234+
onValueChange={props.onToggle1MContext}
235+
disabled={!props.selectedModel?.startsWith("anthropic")}
236+
/>
237+
</View>
238+
</View>
239+
) : null}
209240
</View>
210241

211242
<View style={[styles.sectionBlock, { borderColor: theme.colors.border }]}>
@@ -217,23 +248,39 @@ export function RunSettingsSheet(props: RunSettingsSheetProps): JSX.Element {
217248
<Pressable
218249
key={modeOption}
219250
onPress={() => props.onSelectMode(modeOption)}
220-
style={({ pressed }) => [
221-
styles.modeCard,
222-
{
223-
borderColor:
224-
props.mode === modeOption ? theme.colors.accent : theme.colors.border,
225-
backgroundColor:
226-
props.mode === modeOption
227-
? theme.colors.surface
228-
: theme.colors.surfaceSecondary,
229-
opacity: pressed ? 0.85 : 1,
230-
},
231-
]}
251+
style={({ pressed }) => {
252+
const isSelected = props.mode === modeOption;
253+
const selectedFill =
254+
modeOption === "plan" ? theme.colors.planMode : theme.colors.execMode;
255+
return [
256+
styles.modeCard,
257+
{
258+
borderColor: isSelected ? selectedFill : theme.colors.border,
259+
backgroundColor: isSelected ? selectedFill : theme.colors.surfaceSecondary,
260+
opacity: pressed ? 0.85 : 1,
261+
},
262+
];
263+
}}
232264
>
233-
<ThemedText weight="semibold" style={{ textTransform: "capitalize" }}>
265+
<ThemedText
266+
weight="semibold"
267+
style={{
268+
textTransform: "capitalize",
269+
color: props.mode === modeOption
270+
? theme.colors.foregroundInverted
271+
: theme.colors.foregroundPrimary,
272+
}}
273+
>
234274
{modeOption}
235275
</ThemedText>
236-
<ThemedText variant="caption" style={{ color: theme.colors.foregroundMuted }}>
276+
<ThemedText
277+
variant="caption"
278+
style={{
279+
color: props.mode === modeOption
280+
? theme.colors.foregroundInverted
281+
: theme.colors.foregroundMuted,
282+
}}
283+
>
237284
{modeOption === "plan" ? "Plan before executing" : "Act directly"}
238285
</ThemedText>
239286
</Pressable>
@@ -280,24 +327,6 @@ export function RunSettingsSheet(props: RunSettingsSheetProps): JSX.Element {
280327
</View>
281328
</View>
282329

283-
{props.supportsExtendedContext && (
284-
<View style={[styles.sectionBlock, { borderColor: theme.colors.border }]}>
285-
<ThemedText variant="label" weight="semibold" style={styles.sectionTitle}>
286-
Context window
287-
</ThemedText>
288-
<View style={styles.contextRow}>
289-
<ThemedText style={{ flex: 1 }}>
290-
Enable 1M token context (Anthropic only)
291-
</ThemedText>
292-
<Switch
293-
value={props.use1MContext}
294-
onValueChange={props.onToggle1MContext}
295-
trackColor={{ true: theme.colors.accent, false: theme.colors.border }}
296-
thumbColor={theme.colors.surface}
297-
/>
298-
</View>
299-
</View>
300-
)}
301330
</ScrollView>
302331
</View>
303332
</Modal>
@@ -362,7 +391,7 @@ const styles = StyleSheet.create({
362391
marginTop: 8,
363392
},
364393
modelList: {
365-
maxHeight: 320,
394+
marginTop: 8,
366395
},
367396
chip: {
368397
paddingVertical: 6,
@@ -396,9 +425,4 @@ const styles = StyleSheet.create({
396425
paddingHorizontal: 12,
397426
borderRadius: 999,
398427
},
399-
contextRow: {
400-
flexDirection: "row",
401-
alignItems: "center",
402-
gap: 12,
403-
},
404428
});
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { FlatList, Pressable, View } from "react-native";
2+
import type { SlashSuggestion } from "@shared/utils/slashCommands/types";
3+
import { ThemedText } from "./ThemedText";
4+
import { useTheme } from "../theme";
5+
6+
interface SlashCommandSuggestionsProps {
7+
suggestions: SlashSuggestion[];
8+
visible: boolean;
9+
highlightedIndex: number;
10+
listId: string;
11+
onSelect: (suggestion: SlashSuggestion) => void;
12+
onHighlight: (index: number) => void;
13+
}
14+
15+
export function SlashCommandSuggestions(props: SlashCommandSuggestionsProps) {
16+
const theme = useTheme();
17+
18+
if (!props.visible || props.suggestions.length === 0) {
19+
return null;
20+
}
21+
22+
return (
23+
<View
24+
style={{
25+
position: "absolute",
26+
left: 0,
27+
right: 0,
28+
bottom: "100%",
29+
marginBottom: theme.spacing.xs,
30+
borderRadius: theme.radii.md,
31+
borderWidth: 1,
32+
borderColor: theme.colors.border,
33+
backgroundColor: theme.colors.surface,
34+
shadowColor: "#000",
35+
shadowOpacity: 0.2,
36+
shadowOffset: { width: 0, height: 4 },
37+
shadowRadius: 12,
38+
elevation: 4,
39+
zIndex: 20,
40+
}}
41+
>
42+
<FlatList
43+
data={props.suggestions}
44+
keyExtractor={(item) => item.id}
45+
listKey={props.listId}
46+
keyboardShouldPersistTaps="handled"
47+
renderItem={({ item, index }) => {
48+
const highlighted = index === props.highlightedIndex;
49+
return (
50+
<Pressable
51+
onPress={() => props.onSelect(item)}
52+
onHoverIn={() => props.onHighlight(index)}
53+
onPressIn={() => props.onHighlight(index)}
54+
style={({ pressed }) => ({
55+
paddingVertical: theme.spacing.sm,
56+
paddingHorizontal: theme.spacing.md,
57+
backgroundColor: highlighted
58+
? theme.colors.surfaceSecondary
59+
: pressed
60+
? theme.colors.surfaceTertiary
61+
: theme.colors.surface,
62+
flexDirection: "row",
63+
alignItems: "center",
64+
justifyContent: "space-between",
65+
gap: theme.spacing.md,
66+
})}
67+
>
68+
<ThemedText weight="semibold" style={{ color: theme.colors.accent }}>
69+
{item.display}
70+
</ThemedText>
71+
<ThemedText
72+
numberOfLines={1}
73+
style={{ flex: 1, color: theme.colors.foregroundMuted, textAlign: "right" }}
74+
>
75+
{item.description}
76+
</ThemedText>
77+
</Pressable>
78+
);
79+
}}
80+
style={{ maxHeight: 240 }}
81+
/>
82+
</View>
83+
);
84+
}

0 commit comments

Comments
 (0)