Skip to content

Commit 0f80b35

Browse files
committed
feat: /name command to rename a session from the phone
Adds a client-side /name command that renames the current session. It sends a 'name' control action which the pino-mirror extension applies via pi.setSessionName(); the existing syncTitle() then pushes a host.retitle so the session list updates. - pino-mirror: handle the 'name' action, set the pi session name, and re-sync the title to the server. - app: /name <text> (or a dialog when no text is given). Client commands now receive the inline argument after the command word.
1 parent 3c5087f commit 0f80b35

2 files changed

Lines changed: 69 additions & 9 deletions

File tree

app/lib/ui/composer/client_commands.dart

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ typedef ClientCmdHandler =
2020
BuildContext context,
2121
WidgetRef ref, {
2222
required String sessionId,
23+
required String arg,
2324
});
2425

2526
class ClientCommand {
@@ -48,9 +49,10 @@ Future<bool> handleClientCommand(
4849
if (!raw.startsWith('/')) return false;
4950
final firstSpace = raw.indexOf(RegExp(r'\s'));
5051
final name = firstSpace < 0 ? raw.substring(1) : raw.substring(1, firstSpace);
52+
final arg = firstSpace < 0 ? '' : raw.substring(firstSpace + 1).trim();
5153
for (final c in clientCommands) {
5254
if (c.name == name) {
53-
await c.handler(context, ref, sessionId: sessionId);
55+
await c.handler(context, ref, sessionId: sessionId, arg: arg);
5456
return true;
5557
}
5658
}
@@ -61,7 +63,7 @@ final List<ClientCommand> clientCommands = <ClientCommand>[
6163
ClientCommand(
6264
name: 'new',
6365
description: 'Start a fresh agent session in the same project',
64-
handler: (context, ref, {required sessionId}) async {
66+
handler: (context, ref, {required sessionId, required arg}) async {
6567
final session = ref.read(sessionsProvider).byId(sessionId);
6668
if (session == null) return;
6769
final messenger = ScaffoldMessenger.of(context);
@@ -81,7 +83,7 @@ final List<ClientCommand> clientCommands = <ClientCommand>[
8183
ClientCommand(
8284
name: 'cancel',
8385
description: 'Cancel the current agent turn',
84-
handler: (context, ref, {required sessionId}) async {
86+
handler: (context, ref, {required sessionId, required arg}) async {
8587
try {
8688
await ref.read(connectionControllerProvider.notifier).request(
8789
MsgType.cmd,
@@ -95,7 +97,7 @@ final List<ClientCommand> clientCommands = <ClientCommand>[
9597
ClientCommand(
9698
name: 'unpair',
9799
description: 'Forget the paired desktop server and return to pairing',
98-
handler: (context, ref, {required sessionId}) async {
100+
handler: (context, ref, {required sessionId, required arg}) async {
99101
await ref.read(connectionControllerProvider.notifier).unpair();
100102
if (!context.mounted) return;
101103
context.go('/pair');
@@ -104,7 +106,7 @@ final List<ClientCommand> clientCommands = <ClientCommand>[
104106
ClientCommand(
105107
name: 'help',
106108
description: 'Show available commands',
107-
handler: (context, ref, {required sessionId}) async {
109+
handler: (context, ref, {required sessionId, required arg}) async {
108110
final agentCmds = ref.read(commandsProvider(sessionId));
109111
final lines = [
110112
for (final c in clientCommands) '/${c.name} — ${c.description}',
@@ -138,7 +140,7 @@ final List<ClientCommand> clientCommands = <ClientCommand>[
138140
name: 'ask',
139141
description:
140142
'Debug: ask the server to ask you a question (round-trip test)',
141-
handler: (context, ref, {required sessionId}) async {
143+
handler: (context, ref, {required sessionId, required arg}) async {
142144
try {
143145
await ref.read(connectionControllerProvider.notifier).request(
144146
MsgType.cmd,
@@ -152,7 +154,7 @@ final List<ClientCommand> clientCommands = <ClientCommand>[
152154
ClientCommand(
153155
name: 'compact',
154156
description: 'Compact the conversation to free up context',
155-
handler: (context, ref, {required sessionId}) async {
157+
handler: (context, ref, {required sessionId, required arg}) async {
156158
final meta = ref.read(sessionMetaProvider(sessionId));
157159
if (meta == null) {
158160
ScaffoldMessenger.of(context).showSnackBar(
@@ -171,7 +173,7 @@ final List<ClientCommand> clientCommands = <ClientCommand>[
171173
ClientCommand(
172174
name: 'thinking',
173175
description: 'Set the agent thinking level',
174-
handler: (context, ref, {required sessionId}) async {
176+
handler: (context, ref, {required sessionId, required arg}) async {
175177
final level = await _pickThinkingLevel(context);
176178
if (level == null || !context.mounted) return;
177179
ref
@@ -185,7 +187,7 @@ final List<ClientCommand> clientCommands = <ClientCommand>[
185187
ClientCommand(
186188
name: 'model',
187189
description: 'Switch the agent model',
188-
handler: (context, ref, {required sessionId}) async {
190+
handler: (context, ref, {required sessionId, required arg}) async {
189191
final meta = ref.read(sessionMetaProvider(sessionId));
190192
final models = meta?.models ?? const [];
191193
if (models.isEmpty) {
@@ -216,8 +218,53 @@ final List<ClientCommand> clientCommands = <ClientCommand>[
216218
).showSnackBar(SnackBar(content: Text('Switching to ${picked.name}…')));
217219
},
218220
),
221+
ClientCommand(
222+
name: 'name',
223+
description: 'Rename this session (shown in the session list)',
224+
handler: (context, ref, {required sessionId, required arg}) async {
225+
final title = arg.isNotEmpty ? arg : await _promptSessionName(context);
226+
if (title == null || title.isEmpty || !context.mounted) return;
227+
ref
228+
.read(storeControllerProvider.notifier)
229+
.sendSessionAction(sessionId, 'name', args: {'name': title});
230+
ScaffoldMessenger.of(
231+
context,
232+
).showSnackBar(SnackBar(content: Text('Renamed to “$title”')));
233+
},
234+
),
219235
];
220236

237+
/// Prompt for a session name via a simple text dialog. Resolves with the
238+
/// trimmed name, or null if dismissed/empty.
239+
Future<String?> _promptSessionName(BuildContext context) async {
240+
final controller = TextEditingController();
241+
final result = await showDialog<String>(
242+
context: context,
243+
builder: (ctx) => AlertDialog(
244+
title: const Text('Rename session'),
245+
content: TextField(
246+
controller: controller,
247+
autofocus: true,
248+
textInputAction: TextInputAction.done,
249+
decoration: const InputDecoration(hintText: 'Session name'),
250+
onSubmitted: (v) => Navigator.pop(ctx, v.trim()),
251+
),
252+
actions: [
253+
TextButton(
254+
onPressed: () => Navigator.pop(ctx),
255+
child: const Text('Cancel'),
256+
),
257+
TextButton(
258+
onPressed: () => Navigator.pop(ctx, controller.text.trim()),
259+
child: const Text('Rename'),
260+
),
261+
],
262+
),
263+
);
264+
controller.dispose();
265+
return result;
266+
}
267+
221268
/// pi's thinking levels, low → high. `off` disables reasoning.
222269
const _thinkingLevels = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh'];
223270

server/extensions/pino-mirror.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,19 @@ export default function (pi: ExtensionAPI): void {
9494
pi.setThinkingLevel(level as never);
9595
emitMeta();
9696
}
97+
} else if (action === "name") {
98+
const name = typeof args?.name === "string" ? args.name.trim() : "";
99+
if (!name) {
100+
emitActionError("name", "name required");
101+
return;
102+
}
103+
try {
104+
pi.setSessionName(name);
105+
cachedSessionName = name;
106+
syncTitle();
107+
} catch {
108+
emitActionError("name", "could not set session name");
109+
}
97110
} else if (action === "model") {
98111
const provider = typeof args?.provider === "string" ? args.provider : undefined;
99112
const id = typeof args?.id === "string" ? args.id : undefined;

0 commit comments

Comments
 (0)