@letta-ai/letta-code 0.27.16 → 0.27.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letta-ai/letta-code",
3
- "version": "0.27.16",
3
+ "version": "0.27.18",
4
4
  "description": "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.3.0",
@@ -63,6 +63,23 @@ const RULES = [
63
63
  description:
64
64
  "telemetry/ is a leaf observer — it must not import from cli/, agent/, websocket/, or tools/ (only backend/api/ for submitting data is permitted)",
65
65
  },
66
+ {
67
+ layer: "sandbox",
68
+ forbidden: [
69
+ "cli",
70
+ "agent",
71
+ "tools",
72
+ "websocket",
73
+ "backend",
74
+ "providers",
75
+ "permissions",
76
+ "channels",
77
+ "cron",
78
+ "telemetry",
79
+ ],
80
+ description:
81
+ "sandbox/ is a pure leaf — it generates sandbox argv/profiles from plain paths. It must not import from any domain layer; callers resolve paths and pass them in",
82
+ },
66
83
  ];
67
84
 
68
85
  // Matches: import ... from "@/forbidden/..." (static imports only)
@@ -80,6 +80,7 @@ const ciDirs = [
80
80
  "src/providers",
81
81
  "src/queue",
82
82
  "src/reminders",
83
+ "src/sandbox",
83
84
  "src/skills",
84
85
  "src/telemetry",
85
86
  "src/test-utils",
@@ -20,6 +20,7 @@ const dirs = [
20
20
  "src/providers",
21
21
  "src/queue",
22
22
  "src/reminders",
23
+ "src/sandbox",
23
24
  "src/skills",
24
25
  "src/telemetry",
25
26
  "src/test-utils",
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: creating-mods
3
- description: Creates and edits trusted local Letta Code mods, including tools, slash commands, local-only model providers, lifecycle/turn events, scoped conversation helpers, panels, status values, and capability-gated behavior. Use when asked to make a mod, add an agent-callable tool, add a slash command, add a local provider/model adapter, transform turns, react to app events, or add lightweight mod UI outside the dedicated /statusline flow.
3
+ description: Creates and edits trusted local Letta Code mods, including tools, slash commands, local-only model providers, lifecycle/turn events, scoped conversation helpers, panels, and capability-gated behavior. Use when asked to make a mod, add an agent-callable tool, add a slash command, add a local provider/model adapter, transform turns, react to app events, or add lightweight mod UI outside the dedicated /statusline flow.
4
4
  ---
5
5
 
6
6
  # Creating Mods
7
7
 
8
- Use this skill to create or update trusted Letta Code mod files. Mods are trusted local code that add small composable capabilities through mod APIs, not by importing app internals. Dynamic agent/conversation/workspace/model state is passed as `ctx` to tool, command, event, permission, status, and statusline callbacks; do not read mutable global context for model-callable behavior. Prefer scoped handles (`ctx.conversation`, `ctx.cwd`, `ctx.agent`) and guard optional UI with `letta.capabilities`.
8
+ Use this skill to create or update trusted Letta Code mod files. Mods are trusted local code that add small composable capabilities through mod APIs, not by importing app internals. Dynamic agent/conversation/workspace/model state is passed as `ctx` to tool, command, event, and permission callbacks (panels receive live `agent`/`model` in their render context); do not read mutable global context for model-callable behavior. Prefer scoped handles (`ctx.conversation`, `ctx.cwd`, `ctx.agent`) and guard optional UI with `letta.capabilities`.
9
9
 
10
10
  Capabilities vary by surface. TUI/headless may load tools, commands, events, UI, and providers; the desktop listener loads provider-only mods for local provider discovery. Always guard optional capabilities.
11
11
 
@@ -86,11 +86,11 @@ letta.capabilities.commands
86
86
  letta.capabilities.events.lifecycle
87
87
  letta.capabilities.events.tools
88
88
  letta.capabilities.events.turns
89
+ letta.capabilities.events.compact
90
+ letta.capabilities.events.llm
89
91
  letta.capabilities.permissions
90
92
  letta.capabilities.providers
91
93
  letta.capabilities.ui.panels
92
- letta.capabilities.ui.statusValues
93
- letta.capabilities.ui.customStatuslineRenderer
94
94
  ```
95
95
 
96
96
  ## Scoped API model
@@ -135,7 +135,7 @@ Before finishing, verify:
135
135
  - Command/tool IDs are valid; command overrides of built-ins are intentional, and tool IDs do not collide with built-ins.
136
136
  - Tool descriptions explain when the model should call them.
137
137
  - JSON schemas are object schemas with useful descriptions.
138
- - Optional UI/event/statusline APIs are capability-guarded.
138
+ - Optional UI/event APIs are capability-guarded.
139
139
  - Provider mods are capability-guarded and clearly documented as local-agent only.
140
140
  - Timers, intervals, event registrations, and panels are cleaned up in a disposer.
141
141
  - Busy commands return `{ type: "handled" }` quickly and avoid main-conversation sends.
@@ -152,7 +152,7 @@ Before finishing, verify:
152
152
  | `references/providers.md` | Adding a custom model/API provider for local agents |
153
153
  | `references/events.md` | Reacting to lifecycle/tool/turn events or transforming turns/tools |
154
154
  | `references/permissions.md` | Enforcing dynamic tool allow/ask/deny policy before approval/execution |
155
- | `references/ui.md` | Panels, status values, or statusline capability guards are involved |
155
+ | `references/ui.md` | Panels (including order-0 statusline) or `ui.panels` capability guards are involved |
156
156
  | `references/plan-mode.md` | Recreating plan mode with commands, tools, events, permissions, and local state |
157
157
  | `references/analysis-mode.md` | Phrase-triggered diagnostic mode with turn reminders (simpler than plan-mode) |
158
158
  | `references/architecture.md` | Multiple capabilities, local state, cleanup, background model work, or non-trivial composition |
@@ -78,14 +78,22 @@ const stream = await forked.sendMessageStream([
78
78
 
79
79
  Do not call `ctx.conversation.sendMessageStream()` on the active conversation from a busy command; direct sends can conflict with the active run.
80
80
 
81
- ### Event + status value
81
+ ### Event + panel
82
82
 
83
- Use lifecycle events to maintain small status values such as active conversation state. Guard both event and status capabilities.
83
+ Use lifecycle events to maintain a small panel such as active conversation state. Guard both event and panel capabilities, and re-render with `panel.update()`.
84
84
 
85
85
  ```ts
86
- if (letta.capabilities.events.lifecycle && letta.capabilities.ui.statusValues) {
86
+ if (letta.capabilities.events.lifecycle && letta.capabilities.ui.panels) {
87
+ let conversation = "";
88
+ const panel = letta.ui.openPanel({
89
+ id: "conversation",
90
+ order: 100,
91
+ render: ({ width, row }) => row("conversation", conversation, width),
92
+ });
93
+ disposers.push(() => panel.close());
87
94
  disposers.push(letta.events.on("conversation_open", (event) => {
88
- letta.ui.setStatus("conversation", event.reason);
95
+ conversation = event.reason;
96
+ panel.update();
89
97
  }));
90
98
  }
91
99
  ```
@@ -126,10 +134,13 @@ ctx.conversation.id // string | null
126
134
  ctx.conversation.getHistory(opts) // recent messages
127
135
  ctx.conversation.fork(opts) // returns a scoped handle
128
136
  ctx.conversation.sendMessageStream(messages, opts)
137
+ ctx.conversation.updateLlmConfig(opts) // change model / reasoning effort / context window
129
138
  ```
130
139
 
131
140
  A forked handle keeps the same agent/backend defaults and targets the forked conversation. Use forked handles for background model work. Use `getHistory({ limit, order, includeErrors })` when local logic needs conversation context.
132
141
 
142
+ `updateLlmConfig({ model?, reasoningEffort?, contextWindow?, scope? })` changes the model, reasoning effort, and/or context window, and works across local and Constellation backends. Only the fields you pass change; the rest are preserved, so `updateLlmConfig({ contextWindow })` adjusts just the context window without touching the model or reasoning effort. `scope` defaults to `"conversation"` (a conversation-scoped override that leaves the agent's default untouched); pass `scope: "agent"` to change the agent default. Changing reasoning effort without a model resolves the current model to rebuild provider-specific settings. The change takes effect on the next turn (the model is resolved per provider request).
143
+
133
144
  Tools currently receive `ctx.conversation.getHistory()` but not fork/send helpers. If a tool needs model-side follow-up, return information for the model to act on instead of starting a hidden run from the tool.
134
145
 
135
146
  ## Error handling
@@ -96,9 +96,10 @@ export default function activate(letta) {
96
96
  return { type: "output", output: `hello ${ctx.args || "there"}` };
97
97
  }
98
98
 
99
+ const greeting = `hello ${ctx.args || "there"}`;
99
100
  const panel = letta.ui.openPanel({
100
101
  id: "hello-panel",
101
- content: [`hello ${ctx.args || "there"}`],
102
+ render: () => greeting,
102
103
  });
103
104
  setTimeout(() => panel.close(), 5_000);
104
105
  return { type: "handled" };
@@ -20,6 +20,8 @@ This is the first slice of the hooks-v2 direction. The long-term goal is for typ
20
20
  letta.capabilities.events.lifecycle
21
21
  letta.capabilities.events.tools
22
22
  letta.capabilities.events.turns
23
+ letta.capabilities.events.compact
24
+ letta.capabilities.events.llm
23
25
  ```
24
26
 
25
27
  Guard events when writing portable mods:
@@ -55,9 +57,11 @@ letta.events.on("tool_start", (event, ctx) => {
55
57
  });
56
58
  ```
57
59
 
58
- Lifecycle, turn-start, and tool-start events are wired today.
60
+ Lifecycle, turn, tool, compaction, and llm events are wired today.
59
61
 
60
- Lifecycle handlers are notification-only and should not return values. `turn_start` handlers can transform the outbound input for the next model turn. `tool_start` handlers can transform the tool arguments before execution.
62
+ Lifecycle handlers are notification-only and should not return values. `turn_start` handlers can transform the outbound input for the next model turn. `tool_start` handlers can transform the tool arguments before execution. Compaction and llm handlers are notification-only.
63
+
64
+ `compact_start`/`compact_end` and `llm_start`/`llm_end` only fire on the **local backend**, where compaction and provider requests run client-side. On the constellation backend that work happens server-side and these events do not fire, so guard with `letta.capabilities.events.compact` / `letta.capabilities.events.llm` for portable mods.
61
65
 
62
66
  ## Supported events
63
67
 
@@ -65,7 +69,12 @@ Lifecycle handlers are notification-only and should not return values. `turn_sta
65
69
  "conversation_open"
66
70
  "conversation_close"
67
71
  "tool_start"
72
+ "tool_end"
68
73
  "turn_start"
74
+ "compact_start"
75
+ "compact_end"
76
+ "llm_start"
77
+ "llm_end"
69
78
  ```
70
79
 
71
80
  `conversation_open` event:
@@ -138,6 +147,43 @@ Handlers run in registration order. Later handlers see the current args after ea
138
147
 
139
148
  `tool_start` is intentionally a trusted local mod point: it can rewrite commands, file paths, and other tool inputs before execution. Keep transforms focused and unsurprising.
140
149
 
150
+ `tool_end` event:
151
+
152
+ ```ts
153
+ {
154
+ agentId: string | null;
155
+ conversationId: string | null;
156
+ toolCallId: string | null;
157
+ toolName: string;
158
+ status: "success" | "error";
159
+ output: string;
160
+ }
161
+ ```
162
+
163
+ `tool_end` fires immediately after a tool produces a result, before the agent sees it. Handlers can inspect the result, or return `{ result: { status, output } }` to replace it:
164
+
165
+ ```ts
166
+ letta.events.on("tool_end", (event) => {
167
+ if (event.toolName !== "Bash" || event.status !== "success") return;
168
+ return { result: { status: "success", output: redactSecrets(event.output) } };
169
+ });
170
+ ```
171
+
172
+ The first handler that returns a `result` wins; later handlers are shadowed. Only string results are surfaced — multimodal/image results pass through unchanged. `tool_end` is the trusted-local-mod equivalent of the `PostToolUse` / `PostToolUseFailure` hooks for observing and rewriting tool results.
173
+
174
+ A handler can also react to a specific tool completing by adjusting conversation state. For example, switch model and reasoning effort when entering and exiting plan mode (`tool_end` fires only after the tool succeeds, so a denied approval won't switch):
175
+
176
+ ```ts
177
+ letta.events.on("tool_end", async (event, ctx) => {
178
+ if (event.status !== "success") return;
179
+ if (event.toolName === "enter_plan_mode") {
180
+ await ctx.conversation.updateLlmConfig({ model: "anthropic/claude-opus-4-8", reasoningEffort: "high" });
181
+ } else if (event.toolName === "exit_plan_mode") {
182
+ await ctx.conversation.updateLlmConfig({ model: "openai/gpt-5.5", reasoningEffort: "max" });
183
+ }
184
+ });
185
+ ```
186
+
141
187
  `turn_start` fires before outbound turns that include a user message. In the TUI this includes normal submits and prompt-style command turns. In headless it includes one-shot prompts and bidirectional user turns.
142
188
 
143
189
  Handlers can mutate `event.input` directly or return replacement input:
@@ -170,6 +216,67 @@ Handlers run in registration order. Later handlers see the current input after e
170
216
 
171
217
  `turn_start` is intentionally a trusted local mod point: it can rewrite user messages, approval results, and ordering. Keep transforms focused and unsurprising.
172
218
 
219
+ `compact_start` event:
220
+
221
+ ```ts
222
+ {
223
+ agentId: string | null;
224
+ conversationId: string | null;
225
+ trigger: "manual" | "context_window_overflow" | "context_window_limit";
226
+ }
227
+ ```
228
+
229
+ `compact_start` fires before the local backend compacts a conversation, while the full message history is still in context. `trigger` distinguishes a manual `/compact` from the two automatic triggers (provider context-window overflow, and exceeding the configured context window). Use it to checkpoint state before eviction.
230
+
231
+ `compact_end` event:
232
+
233
+ ```ts
234
+ {
235
+ agentId: string | null;
236
+ conversationId: string | null;
237
+ trigger: "manual" | "context_window_overflow" | "context_window_limit";
238
+ messagesBefore: number;
239
+ messagesAfter: number;
240
+ contextTokensBefore: number;
241
+ contextTokensAfter: number;
242
+ }
243
+ ```
244
+
245
+ `compact_end` fires after compaction completes, carrying the before/after message and context-token counts. Both events are notification-only; return values are ignored. A throwing handler is isolated and never breaks compaction.
246
+
247
+ `llm_start` event:
248
+
249
+ ```ts
250
+ {
251
+ agentId: string | null;
252
+ conversationId: string | null;
253
+ model: string;
254
+ messageCount: number;
255
+ contextWindow: number;
256
+ }
257
+ ```
258
+
259
+ `llm_start` fires right before each provider request, with the model handle, the number of messages being sent, and the model's context window. It fires once per provider request, so a retry or an overflow-triggered re-request emits another `llm_start`.
260
+
261
+ `llm_end` event:
262
+
263
+ ```ts
264
+ {
265
+ agentId: string | null;
266
+ conversationId: string | null;
267
+ model: string;
268
+ stopReason: string;
269
+ usage: {
270
+ promptTokens: number;
271
+ completionTokens: number;
272
+ totalTokens: number;
273
+ };
274
+ durationMs: number;
275
+ }
276
+ ```
277
+
278
+ `llm_end` fires once a provider request produces a final message, carrying the stop reason, token usage, and wall-clock duration. A request that fails before producing a final message (e.g. a transport error that triggers a retry) emits no `llm_end`. Both events are notification-only; return values are ignored. A throwing handler is isolated and never breaks the provider request.
279
+
173
280
  Handlers also receive:
174
281
 
175
282
  ```ts
@@ -199,12 +306,23 @@ export default function activate(letta) {
199
306
  if (!letta.capabilities.events.lifecycle) return;
200
307
 
201
308
  const disposers = [];
202
-
203
- disposers.push(
204
- letta.events.on("conversation_open", (event) => {
205
- letta.ui.setStatus("conversation", event.reason);
206
- }),
207
- );
309
+ let conversation = "";
310
+
311
+ if (letta.capabilities.ui.panels) {
312
+ const panel = letta.ui.openPanel({
313
+ id: "conversation",
314
+ order: 100,
315
+ render: ({ width, row }) => row("conversation", conversation, width),
316
+ });
317
+ disposers.push(() => panel.close());
318
+
319
+ disposers.push(
320
+ letta.events.on("conversation_open", (event) => {
321
+ conversation = event.reason;
322
+ panel.update();
323
+ }),
324
+ );
325
+ }
208
326
 
209
327
  disposers.push(
210
328
  letta.events.on("conversation_close", (event) => {
@@ -214,7 +332,6 @@ export default function activate(letta) {
214
332
 
215
333
  return () => {
216
334
  for (const dispose of disposers.reverse()) dispose();
217
- letta.ui.clearStatus("conversation");
218
335
  };
219
336
  }
220
337
  ```
@@ -42,7 +42,7 @@ Guard each registration with the matching capability:
42
42
  - `events.turns`: append a focused plan-mode reminder while active
43
43
  - `permissions`: block mutating tools except planning coordination tools and plan-file writes
44
44
 
45
- Do not use panels for persistent mode state. Panels are transient UI and can be noisy/fragile for mode indicators. Do not add a custom statusline renderer just to show plan mode; `setStatuslineRenderer` is a single global renderer, not an additive slot. This example intentionally keeps visible mode state out of scope.
45
+ Do not use panels for persistent mode state. Panels are transient UI and can be noisy/fragile for mode indicators. Do not claim the order-0 statusline panel just to show plan mode; that slot is a single primary line, not an additive indicator. This example intentionally keeps visible mode state out of scope.
46
46
 
47
47
  ## State
48
48
 
@@ -1,6 +1,6 @@
1
1
  # Mod UI recipes
2
2
 
3
- UI capabilities are optional. Always guard UI work with `letta.capabilities.ui.*` when writing portable mods.
3
+ UI capabilities are optional. Always guard UI work with `letta.capabilities.ui.panels` when writing portable mods.
4
4
 
5
5
  For UI that belongs to a larger command/event mod, also read `architecture.md` for cleanup and composition patterns.
6
6
 
@@ -8,13 +8,9 @@ For UI that belongs to a larger command/event mod, also read `architecture.md` f
8
8
 
9
9
  ```ts
10
10
  letta.capabilities.ui.panels
11
- letta.capabilities.ui.statusValues
12
- letta.capabilities.ui.customStatuslineRenderer
13
11
  ```
14
12
 
15
- - `panels`: transient text blocks above the input bar.
16
- - `statusValues`: small named status data that renderers or future surfaces can display.
17
- - `customStatuslineRenderer`: TUI-only custom bottom-row renderer. Use `customizing-statusline` for statusline work.
13
+ - `panels`: text blocks placed around the input bar (the only mod UI surface). Desktop/listener disables panel UI.
18
14
 
19
15
  ## Panels
20
16
 
@@ -22,50 +18,69 @@ Panels are app/TUI-global today. Desktop/listener disables panel UI; future scop
22
18
 
23
19
  ```ts
24
20
  if (letta.capabilities.ui.panels) {
21
+ let status = "Working…";
25
22
  const panel = letta.ui.openPanel({
26
23
  id: "my-mod",
27
- content: ["Working…"],
28
24
  order: 100,
25
+ render: ({ width }) => status,
29
26
  });
30
27
 
31
- panel.update({ content: ["Done"] });
28
+ status = "Done";
29
+ panel.update(); // re-invokes render with the current width
32
30
  setTimeout(() => panel.close(), 5_000);
33
31
  }
34
32
  ```
35
33
 
36
- Panel content is plain text: a string or string array. Keep it short; use command `output` for longer text.
34
+ `render` returns the panel body: a string or string array (one entry per line). The host owns the region — it clips each line to `width` and caps total height — so the mod owns layout: use `width`, `row(left, right, width)`, and `columns(parts, width)` to align. The host re-invokes `render` whenever you call `panel.update()` and on terminal resize. Keep `render` cheap and side-effect-free; for longer text use a command `output` instead.
37
35
 
38
- Close panels when they are transient, and close/replace long-lived panels from the activation disposer if reload should remove them.
36
+ ### Placement by `order`
39
37
 
40
- ## Status values
38
+ `order` is a signed coordinate around the input:
41
39
 
42
- ```ts
43
- if (letta.capabilities.ui.statusValues) {
44
- letta.ui.setStatus("branch", "main");
45
- }
46
- ```
40
+ - `order > 0` — above the input, higher nearer the top (default `100`).
41
+ - `order === 0` — the primary line just below the input, overriding the built-in `agent · model`. This is the statusline slot; use `customizing-statusline` for that work.
42
+ - `order < 0` — stacks below the primary line, `-1` closest.
43
+
44
+ A panel whose `render` is empty (`""`, `[]`, or only blank lines) is hidden.
47
45
 
48
- Clear status values in disposers if they are owned by timers, events, or external state:
46
+ ### Render context
49
47
 
50
48
  ```ts
51
- return () => {
52
- letta.ui.clearStatus("branch");
53
- };
49
+ render(ctx: {
50
+ width: number;
51
+ agent: { id, name };
52
+ model: { id, displayName, provider, reasoningEffort };
53
+ row(left, right, width): string;
54
+ columns(parts: string[], width): string;
55
+ chalk: ChalkInstance;
56
+ }): string | string[]
54
57
  ```
55
58
 
59
+ `row`/`columns` are ANSI-aware, so chalk-colored segments align correctly.
60
+
61
+ Close panels when they are transient, and close/replace long-lived panels from the activation disposer if reload should remove them.
62
+
56
63
  ## Timers and cleanup
57
64
 
58
65
  ```ts
59
66
  export default function activate(letta) {
60
- if (!letta.capabilities.ui.statusValues) return;
67
+ if (!letta.capabilities.ui.panels) return;
68
+
69
+ let clock = new Date().toLocaleTimeString();
70
+ const panel = letta.ui.openPanel({
71
+ id: "clock",
72
+ order: 100,
73
+ render: ({ width, row }) => row("clock", clock, width),
74
+ });
61
75
 
62
- const update = () => letta.ui.setStatus("clock", new Date().toLocaleTimeString());
63
- update();
64
- const timer = setInterval(update, 30_000);
76
+ const timer = setInterval(() => {
77
+ clock = new Date().toLocaleTimeString();
78
+ panel.update();
79
+ }, 30_000);
65
80
 
66
81
  return () => {
67
82
  clearInterval(timer);
68
- letta.ui.clearStatus("clock");
83
+ panel.close();
69
84
  };
70
85
  }
71
86
  ```
@@ -11,28 +11,28 @@ Use this skill to create or update the global Letta Code statusline mod:
11
11
  ~/.letta/mods/statusline.tsx
12
12
  ```
13
13
 
14
- The statusline is a full-row idle renderer. Host UI can still temporarily preempt it for safety confirmations and transient hints.
14
+ The statusline is a panel registered at `order: 0` — the primary line just below the input. It overrides the built-in `agent · model` line. Host UI can still temporarily preempt it for safety confirmations and transient hints.
15
15
 
16
16
  ## Statusline ownership model
17
17
 
18
18
  ```text
19
19
  safety preemption
20
20
  else transient host hint
21
- else custom statusline mod
21
+ else order-0 statusline panel
22
22
  else built-in default statusline
23
23
  ```
24
24
 
25
- A custom statusline owns the whole idle row. Do not preserve legacy left/right split semantics in the new API.
25
+ The order-0 panel owns the whole primary row. It renders text (not React) and owns its own layout via the `row`/`columns` helpers.
26
26
 
27
27
  ## Workflow
28
28
 
29
29
  1. Check whether `~/.letta/mods/statusline.tsx` exists.
30
30
  2. If it exists, read it before editing and preserve unrelated code.
31
- 3. If it does not exist, start from the built-in default template or synthesize a focused starter for the user's request.
31
+ 3. If it does not exist, synthesize a focused starter for the user's request.
32
32
  4. If the user asks to migrate, import a `.sh` file, or match a shell prompt, read `references/migration.md`.
33
33
  5. If API details or concrete patterns are needed, read `references/api.md` and `references/examples.md`.
34
- 6. If the request combines statusline work with commands, tools, events, panels, or stateful mod behavior, also use `creating-mods` and its `references/architecture.md`.
35
- 7. Guard statusline-specific behavior with `letta.capabilities.ui.customStatuslineRenderer` when writing new files.
34
+ 6. If the request combines statusline work with commands, tools, events, other panels, or stateful mod behavior, also use `creating-mods` and its `references/architecture.md`.
35
+ 7. Guard panel work with `letta.capabilities.ui.panels` when writing new files.
36
36
  8. Edit `~/.letta/mods/statusline.tsx`.
37
37
  9. Summarize the absolute file path changed and tell the user to run `/reload` unless the command can reload automatically.
38
38
 
@@ -42,7 +42,7 @@ If the user ran `/statusline` without a specific request:
42
42
 
43
43
  - If a custom statusline file exists, summarize what it appears to do and ask what they want to change.
44
44
  - If no custom file exists, explain that Letta is using the built-in default statusline and offer focused next steps:
45
- 1. start from the default Letta statusline
45
+ 1. start from a simple `agent · model` statusline
46
46
  2. add project info like git branch, worktree, or PR
47
47
  3. migrate an existing legacy statusline `.sh` file
48
48
  4. match shell prompt / PS1
@@ -56,16 +56,15 @@ Keep this conversational. Do not build a menu UI unless the product command expl
56
56
  - Keep the mod single-file for MVP.
57
57
  - Do not assume extra npm packages are available.
58
58
  - Do not use relative multi-file imports yet.
59
- - Keep renderers synchronous. Do not shell, fetch, or await inside render.
60
- - Do async work in setup code, intervals, subscriptions, or status providers.
61
- - Use `letta.ui.setStatus` for data and `setStatuslineRenderer` for drawing that data.
62
- - Guard optional APIs with `letta.capabilities.ui.statusValues` and `letta.capabilities.ui.customStatuslineRenderer` in new files.
63
- - Return a disposer that clears timers/subscriptions.
59
+ - Keep `render` synchronous and side-effect-free. Do not shell, fetch, await, or read files inside render.
60
+ - Do async work in setup code, intervals, or subscriptions, store the result in a closure variable, then call `panel.update()` to re-render.
61
+ - Register the statusline at `order: 0`. Compose left/right with `row(left, right, width)`; color with `chalk`.
62
+ - Guard panel work with `letta.capabilities.ui.panels` in new files.
63
+ - Return a disposer that clears timers/subscriptions and calls `panel.close()`.
64
64
  - Preserve existing mod code unless the user asks to reset.
65
- - Do not delete legacy command statusline files or settings unless the user explicitly asks.
66
65
 
67
66
  ## Useful references
68
67
 
69
- - `references/api.md` - mod API, render context, lifecycle rules
68
+ - `references/api.md` - panel API, render context, lifecycle rules
70
69
  - `references/examples.md` - common statusline patterns
71
70
  - `references/migration.md` - legacy command `.sh` and PS1 migration