Skip to content

Commit 8887718

Browse files
authored
Merge pull request #7 from illegalstudio/feature/rename-sessions
Feature/rename sessions
2 parents 9abf4dd + a61bb12 commit 8887718

16 files changed

Lines changed: 579 additions & 74 deletions

File tree

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ It also surfaces:
4141
| Recent conversation (last 5 messages) | JSONL |
4242
| Last 20 tools used | JSONL |
4343
| Last activity timestamp | JSONL |
44+
| Custom session name | `~/.config/lazyagent/session-names.json` |
4445

4546
## Three interfaces, one binary
4647

@@ -115,7 +116,7 @@ lazyagent --help Show help
115116
| `f` | Cycle activity filter |
116117
| `/` | Search sessions by project path |
117118
| `o` | Open session CWD in editor (see below) |
118-
| `r` | Force refresh |
119+
| `r` | Rename session (empty name resets) |
119120
| `q` / `ctrl+c` | Quit |
120121

121122
### macOS Menu Bar App
@@ -135,7 +136,7 @@ The tray process detaches automatically — your terminal returns immediately. T
135136
| `+` / `-` | Adjust time window (±10 minutes) |
136137
| `f` | Cycle activity filter |
137138
| `/` | Search sessions |
138-
| `r` | Force refresh |
139+
| `r` | Rename session (empty name resets) |
139140
| `esc` | Close detail / dismiss search |
140141

141142
#### Right-click menu
@@ -157,6 +158,8 @@ Starts a read-only HTTP API server on `http://127.0.0.1:7421` (default port, wit
157158
| `GET /api` | Interactive playground (open in browser) |
158159
| `GET /api/sessions` | List visible sessions (`?search=`, `?filter=`) |
159160
| `GET /api/sessions/{id}` | Full session detail |
161+
| `PUT /api/sessions/{id}/name` | Rename session (`{"name": "..."}`, empty resets) |
162+
| `DELETE /api/sessions/{id}/name` | Remove custom name |
160163
| `GET /api/stats` | Summary stats (total, active, window) |
161164
| `GET /api/config` | Current configuration |
162165
| `GET /api/events` | SSE stream for real-time updates |
@@ -280,6 +283,7 @@ make clean
280283
- [x] Token usage and cost estimation in detail panel
281284
- [x] Animated braille spinner for active sessions
282285
- [x] `o` key to open session CWD in editor
286+
- [x] Rename sessions with persistent custom names (`r` key)
283287
- [ ] Display file diff for last written file
284288

285289
### v0.3 — macOS menu bar app
@@ -306,6 +310,7 @@ make clean
306310
- [x] Default port with automatic fallback (7421–7431)
307311
- [x] Custom bind address (`--host`)
308312
- [x] Combinable with TUI and tray (`--tui --tray --api`)
313+
- [x] Session rename endpoints (`PUT`/`DELETE /api/sessions/{id}/name`)
309314

310315
### Future ideas
311316
- [ ] Outbound webhooks on status changes

docs/API.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# lazyagent API
22

3-
lazyagent exposes an HTTP API for monitoring Claude Code sessions. The API is read-only and designed for building external clients (mobile apps, dashboards, integrations).
3+
lazyagent exposes an HTTP API for monitoring and managing Claude Code sessions, designed for building external clients (mobile apps, dashboards, integrations).
44

55
## Starting the API server
66

@@ -47,6 +47,7 @@ List all visible sessions within the configured time window.
4747
"session_id": "abc123",
4848
"cwd": "/Users/me/projects/myapp",
4949
"short_name": "…/projects/myapp",
50+
"custom_name": "my-api-project",
5051
"activity": "thinking",
5152
"is_active": true,
5253
"model": "claude-sonnet-4-20250514",
@@ -73,6 +74,7 @@ Get full details for a specific session.
7374
"session_id": "abc123",
7475
"cwd": "/Users/me/projects/myapp",
7576
"short_name": "…/projects/myapp",
77+
"custom_name": "my-api-project",
7678
"activity": "writing",
7779
"is_active": true,
7880
"model": "claude-sonnet-4-20250514",
@@ -107,6 +109,50 @@ Get full details for a specific session.
107109

108110
---
109111

112+
### PUT /api/sessions/{id}/name
113+
114+
Set a custom name for a session. Names are persisted in `~/.config/lazyagent/session-names.json` and synced across TUI, tray, and API in real-time.
115+
116+
**Request body:**
117+
118+
```json
119+
{
120+
"name": "my-api-project"
121+
}
122+
```
123+
124+
An empty `"name"` resets the session to its default path-based name.
125+
126+
**Response:** `200 OK`
127+
128+
```json
129+
{
130+
"session_id": "abc123",
131+
"custom_name": "my-api-project"
132+
}
133+
```
134+
135+
Triggers an SSE `update` event to all connected clients.
136+
137+
---
138+
139+
### DELETE /api/sessions/{id}/name
140+
141+
Remove the custom name from a session (reset to default path-based name).
142+
143+
**Response:** `200 OK`
144+
145+
```json
146+
{
147+
"session_id": "abc123",
148+
"custom_name": ""
149+
}
150+
```
151+
152+
Triggers an SSE `update` event to all connected clients.
153+
154+
---
155+
110156
### GET /api/stats
111157

112158
Summary statistics.

frontend/src/App.svelte

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,6 @@
7373
} else if (e.key === "-") {
7474
e.preventDefault();
7575
adjustWindow(-10);
76-
} else if (e.key === "r") {
77-
e.preventDefault();
78-
loadSessions();
7976
}
8077
}
8178
@@ -185,7 +182,7 @@
185182
<span><kbd class="text-text/60">/</kbd> search</span>
186183
<span><kbd class="text-text/60">f</kbd> filter</span>
187184
<span><kbd class="text-text/60">+/−</kbd> window</span>
188-
<span><kbd class="text-text/60">r</kbd> refresh</span>
185+
<span><kbd class="text-text/60">r</kbd> rename</span>
189186
<span><kbd class="text-text/60">esc</kbd> back</span>
190187
</footer>
191188
</div>

frontend/src/bindings/github.com/nahime0/lazyagent/internal/tray/models.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export class SessionFull {
4848
"sessionId": string;
4949
"cwd": string;
5050
"shortName": string;
51+
"customName": string;
5152
"activity": string;
5253
"isActive": boolean;
5354
"model": string;
@@ -82,6 +83,9 @@ export class SessionFull {
8283
if (!("shortName" in $$source)) {
8384
this["shortName"] = "";
8485
}
86+
if (!("customName" in $$source)) {
87+
this["customName"] = "";
88+
}
8589
if (!("activity" in $$source)) {
8690
this["activity"] = "";
8791
}
@@ -156,18 +160,18 @@ export class SessionFull {
156160
* Creates a new SessionFull instance from a string or object.
157161
*/
158162
static createFrom($$source: any = {}): SessionFull {
159-
const $$createField10_0 = $$createType0;
160-
const $$createField23_0 = $$createType2;
161-
const $$createField24_0 = $$createType4;
163+
const $$createField11_0 = $$createType0;
164+
const $$createField24_0 = $$createType2;
165+
const $$createField25_0 = $$createType4;
162166
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
163167
if ("sparklineData" in $$parsedSource) {
164-
$$parsedSource["sparklineData"] = $$createField10_0($$parsedSource["sparklineData"]);
168+
$$parsedSource["sparklineData"] = $$createField11_0($$parsedSource["sparklineData"]);
165169
}
166170
if ("recentTools" in $$parsedSource) {
167-
$$parsedSource["recentTools"] = $$createField23_0($$parsedSource["recentTools"]);
171+
$$parsedSource["recentTools"] = $$createField24_0($$parsedSource["recentTools"]);
168172
}
169173
if ("recentMessages" in $$parsedSource) {
170-
$$parsedSource["recentMessages"] = $$createField24_0($$parsedSource["recentMessages"]);
174+
$$parsedSource["recentMessages"] = $$createField25_0($$parsedSource["recentMessages"]);
171175
}
172176
return new SessionFull($$parsedSource as Partial<SessionFull>);
173177
}
@@ -180,6 +184,7 @@ export class SessionItem {
180184
"sessionId": string;
181185
"cwd": string;
182186
"shortName": string;
187+
"customName": string;
183188
"activity": string;
184189
"isActive": boolean;
185190
"model": string;
@@ -200,6 +205,9 @@ export class SessionItem {
200205
if (!("shortName" in $$source)) {
201206
this["shortName"] = "";
202207
}
208+
if (!("customName" in $$source)) {
209+
this["customName"] = "";
210+
}
203211
if (!("activity" in $$source)) {
204212
this["activity"] = "";
205213
}
@@ -232,10 +240,10 @@ export class SessionItem {
232240
* Creates a new SessionItem instance from a string or object.
233241
*/
234242
static createFrom($$source: any = {}): SessionItem {
235-
const $$createField10_0 = $$createType0;
243+
const $$createField11_0 = $$createType0;
236244
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
237245
if ("sparklineData" in $$parsedSource) {
238-
$$parsedSource["sparklineData"] = $$createField10_0($$parsedSource["sparklineData"]);
246+
$$parsedSource["sparklineData"] = $$createField11_0($$parsedSource["sparklineData"]);
239247
}
240248
return new SessionItem($$parsedSource as Partial<SessionItem>);
241249
}

frontend/src/bindings/github.com/nahime0/lazyagent/internal/tray/sessionservice.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ export function GetSessionDetail(id: string): $CancellablePromise<$models.Sessio
4343
});
4444
}
4545

46+
/**
47+
* GetSessionName returns the custom name for a session.
48+
*/
49+
export function GetSessionName(sessionID: string): $CancellablePromise<string> {
50+
return $Call.ByID(3799911173, sessionID);
51+
}
52+
4653
/**
4754
* GetSessions returns all visible sessions for the list view.
4855
*/
@@ -61,6 +68,9 @@ export function GetWindowMinutes(): $CancellablePromise<number> {
6168

6269
/**
6370
* OpenInEditor opens a directory in the user's editor.
71+
* It follows POSIX semantics: $VISUAL is a GUI editor (launched directly),
72+
* $EDITOR is a terminal editor (opened inside a Terminal.app window).
73+
* The config "editor" field is treated as VISUAL (GUI) for backward compatibility.
6474
*/
6575
export function OpenInEditor(cwd: string): $CancellablePromise<void> {
6676
return $Call.ByID(1983870972, cwd);
@@ -80,6 +90,13 @@ export function SetSearchQuery(q: string): $CancellablePromise<void> {
8090
return $Call.ByID(2439661330, q);
8191
}
8292

93+
/**
94+
* SetSessionName stores a custom name for a session. Empty name resets it.
95+
*/
96+
export function SetSessionName(sessionID: string, name: string): $CancellablePromise<void> {
97+
return $Call.ByID(602820185, sessionID, name);
98+
}
99+
83100
/**
84101
* SetWindowMinutes updates the time window.
85102
*/

frontend/src/lib/SessionDetail.svelte

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,94 @@
1212
1313
let detail = $derived($selectedDetail);
1414
let color = $derived(detail ? activityColor(detail.activity) : "var(--color-activity-idle)");
15+
let displayName = $derived(detail ? (detail.customName || detail.shortName) : "");
16+
17+
let renaming = $state(false);
18+
let renameValue = $state("");
19+
let renameInput = $state<HTMLInputElement | null>(null);
1520
1621
function openEditor() {
1722
if (detail) {
1823
SessionService.OpenInEditor(detail.cwd).catch(() => {});
1924
}
2025
}
26+
27+
function startRename() {
28+
if (!detail) return;
29+
renaming = true;
30+
renameValue = detail.customName || "";
31+
requestAnimationFrame(() => renameInput?.focus());
32+
}
33+
34+
function confirmRename() {
35+
if (detail && renaming) {
36+
SessionService.SetSessionName(detail.sessionId, renameValue.trim()).catch(() => {});
37+
}
38+
renaming = false;
39+
renameValue = "";
40+
}
41+
42+
function cancelRename() {
43+
renaming = false;
44+
renameValue = "";
45+
}
46+
47+
function handleRenameKey(e: KeyboardEvent) {
48+
e.stopPropagation(); // Prevent App.svelte from intercepting rename input keys
49+
if (e.key === "Enter") { e.preventDefault(); confirmRename(); }
50+
else if (e.key === "Escape") { e.preventDefault(); cancelRename(); }
51+
}
52+
53+
function handleKeydown(e: KeyboardEvent) {
54+
if (renaming) return; // Already renaming, let handleRenameKey deal with it
55+
const tag = (e.target as HTMLElement)?.tagName;
56+
if (tag === "INPUT" || tag === "TEXTAREA") return;
57+
if (e.key === "r" && detail) {
58+
e.preventDefault();
59+
startRename();
60+
}
61+
}
2162
</script>
2263

64+
<svelte:window onkeydown={handleKeydown} />
65+
2366
{#if detail}
2467
<div class="flex flex-col h-full overflow-y-auto px-4 py-3 gap-3">
2568
<!-- Header -->
2669
<div>
2770
<div class="flex items-center justify-between gap-2">
28-
<h2 class="text-[15px] font-semibold text-text truncate">{detail.shortName}</h2>
29-
<button
30-
class="shrink-0 rounded px-2 py-1 text-[11px] font-medium text-accent bg-accent/10 hover:bg-accent/20 transition-colors no-drag"
31-
onclick={openEditor}
32-
title="Open in editor"
33-
>
34-
Open
35-
</button>
71+
{#if renaming}
72+
<input
73+
bind:this={renameInput}
74+
bind:value={renameValue}
75+
onkeydown={handleRenameKey}
76+
onblur={confirmRename}
77+
class="flex-1 min-w-0 bg-surface text-text text-[15px] font-semibold px-1 py-0 rounded border border-accent outline-none"
78+
placeholder={detail.shortName}
79+
/>
80+
{:else}
81+
<h2
82+
class="text-[15px] font-semibold text-text truncate cursor-pointer hover:text-accent transition-colors"
83+
ondblclick={startRename}
84+
title="Double-click to rename"
85+
>{displayName}</h2>
86+
{/if}
87+
<div class="flex gap-1 shrink-0">
88+
<button
89+
class="rounded px-2 py-1 text-[11px] font-medium text-subtext bg-surface-hover hover:text-text transition-colors no-drag"
90+
onclick={startRename}
91+
title="Rename session"
92+
>
93+
Rename
94+
</button>
95+
<button
96+
class="rounded px-2 py-1 text-[11px] font-medium text-accent bg-accent/10 hover:bg-accent/20 transition-colors no-drag"
97+
onclick={openEditor}
98+
title="Open in editor"
99+
>
100+
Open
101+
</button>
102+
</div>
36103
</div>
37104
<div class="flex items-center gap-2 mt-1">
38105
<ActivityBadge activity={detail.activity} isActive={detail.isActive} />

0 commit comments

Comments
 (0)