@letta-ai/letta-code 0.27.17 → 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.
@@ -12,48 +12,74 @@ This is a trusted, user-owned global mod file. Project mods are intentionally un
12
12
 
13
13
  ## Activation
14
14
 
15
- Export a default function or named `activate` function:
15
+ Export a default function or named `activate` function. The statusline is a panel at `order: 0`:
16
16
 
17
17
  ```tsx
18
18
  export default function activate(letta) {
19
- if (!letta.capabilities.ui.customStatuslineRenderer) return;
20
-
21
- letta.ui.setStatuslineRenderer((context) => {
22
- const { Text } = context.components;
23
- return <Text>{context.agent.name} · {context.model.displayName}</Text>;
19
+ if (!letta.capabilities.ui.panels) return;
20
+
21
+ const panel = letta.ui.openPanel({
22
+ id: "statusline",
23
+ order: 0, // primary line: overrides the built-in agent · model
24
+ render: ({ width, agent, model, row, chalk }) => {
25
+ const left = chalk.cyan(agent.name ?? "Letta");
26
+ const right = chalk.dim(model.displayName ?? "no model");
27
+ return row(left, right, width);
28
+ },
24
29
  });
30
+
31
+ return () => panel.close();
25
32
  }
26
33
  ```
27
34
 
28
35
  ## API
29
36
 
30
37
  ```ts
31
- letta.capabilities.ui.statusValues: boolean
32
- letta.capabilities.ui.customStatuslineRenderer: boolean
38
+ letta.capabilities.ui.panels: boolean
33
39
 
34
- letta.ui.setStatus(key: string, value: string | null | undefined | ((context) => string | null)): void
35
- letta.ui.clearStatus(key: string): void
36
- letta.ui.setStatuslineRenderer(renderer: StatuslineRenderer | ((context) => ReactNode | null)): void
40
+ letta.ui.openPanel(options: {
41
+ id: string;
42
+ order?: number; // default 100
43
+ render: (ctx) => string | string[];
44
+ }): { close(): void; update(opts?: { order?: number }): void }
45
+
46
+ letta.ui.closePanel(id: string): void
37
47
  ```
38
48
 
39
- `setStatus` stores named string values. Renderers read evaluated values from `context.statuses`.
49
+ `openPanel` registers (or replaces, by `id`) a panel. `render` returns the panel body as a string or an array of strings (one per line). Call the returned handle's `update()` to re-render after state changes, and `close()` to remove it.
40
50
 
41
- ```tsx
42
- letta.ui.setStatus("branch", "main");
51
+ ## Order placement
52
+
53
+ `order` is a signed coordinate around the input:
54
+
55
+ - `order > 0` — above the input. Higher numbers render nearer the top. Default when omitted is `100`.
56
+ - `order === 0` — the primary line just below the input. Overrides the built-in `agent · model`. Use this for the statusline.
57
+ - `order < 0` — stacks below the primary line. `-1` sits closest to it, more-negative lower.
58
+
59
+ A panel whose `render` returns empty (`""` or `[]`, or only blank lines) is hidden entirely — no blank row.
43
60
 
44
- letta.ui.setStatuslineRenderer((context) => {
45
- const { Text } = context.components;
46
- return <Text>{context.statuses.branch}</Text>;
47
- });
61
+ ## Render context
62
+
63
+ ```ts
64
+ render(ctx: {
65
+ width: number; // visible columns available to the panel
66
+ agent: { id, name }; // live at render time
67
+ model: { id, displayName, provider, reasoningEffort };
68
+ row(left, right, width): string; // left + right, right-aligned, ANSI-aware
69
+ columns(parts: string[], width): string; // spread parts evenly, ANSI-aware
70
+ chalk: ChalkInstance; // color helper
71
+ }): string | string[]
48
72
  ```
49
73
 
50
- ## Renderer rules
74
+ - `row`/`columns` measure visible width with ANSI stripped, so chalk-colored segments align correctly.
75
+ - The host clips each line to `width` and caps total height; the mod owns layout within that.
76
+
77
+ ## Render rules
51
78
 
52
- - Renderer owns the entire idle bottom row.
53
- - Renderer must be synchronous.
54
- - Do not run shell commands, network requests, file reads, or awaits inside render.
55
- - Do async work in setup code or intervals, store results with `setStatus`, then render `context.statuses`.
56
- - Return `null` only when intentionally rendering nothing.
79
+ - `render` must be synchronous and side-effect-free.
80
+ - Do not run shell commands, network requests, file reads, or awaits inside `render`.
81
+ - Do async work in setup code or intervals, store the result in a closure variable, then call `panel.update()`.
82
+ - Return `""` (or `[]`) to render nothing.
57
83
 
58
84
  ## Async state pattern
59
85
 
@@ -66,92 +92,54 @@ import { promisify } from "node:util";
66
92
  const execFileAsync = promisify(execFile);
67
93
 
68
94
  export default function activate(letta) {
69
- if (!letta.capabilities.ui.customStatuslineRenderer) return;
95
+ if (!letta.capabilities.ui.panels) return;
96
+
97
+ let branch = "";
98
+
99
+ const panel = letta.ui.openPanel({
100
+ id: "statusline",
101
+ order: 0,
102
+ render: ({ width, agent, row, chalk }) => {
103
+ const left = branch ? chalk.green(`\u2442 ${branch}`) : (agent.name ?? "Letta");
104
+ return row(left, "", width);
105
+ },
106
+ });
70
107
 
71
108
  const update = async () => {
72
109
  try {
73
110
  const { stdout } = await execFileAsync("git", ["branch", "--show-current"], {
74
111
  cwd: process.cwd(),
75
112
  });
76
- if (letta.capabilities.ui.statusValues) {
77
- letta.ui.setStatus("branch", stdout.trim());
78
- }
113
+ branch = stdout.trim();
79
114
  } catch {
80
- if (letta.capabilities.ui.statusValues) {
81
- letta.ui.clearStatus("branch");
82
- }
115
+ branch = "";
83
116
  }
117
+ panel.update();
84
118
  };
85
119
 
86
- letta.ui.setStatuslineRenderer((context) => {
87
- const { Text } = context.components;
88
- const branch = context.statuses.branch;
89
- return <Text>{branch ? `branch ${branch}` : context.agent.name}</Text>;
90
- });
91
-
92
120
  void update();
93
121
  const timer = setInterval(update, 30_000);
94
122
 
95
123
  return () => {
96
124
  clearInterval(timer);
97
- if (letta.capabilities.ui.statusValues) {
98
- letta.ui.clearStatus("branch");
99
- }
125
+ panel.close();
100
126
  };
101
127
  }
102
128
  ```
103
129
 
104
- ## Context fields
105
-
106
- The app statusline render context source types live near:
107
-
108
- ```text
109
- src/cli/display/statusline/types.ts
110
- src/cli/display/statusline/context.ts
111
- ```
130
+ ## Full-row layout
112
131
 
113
- Common fields:
132
+ There is no host left/right API. Build left/right alignment inside `render` with `row`:
114
133
 
115
- ```ts
116
- context.components // Display components such as Text, Box, Spacer
117
- context.statuses // evaluated mod status strings
118
- context.app.version
119
- context.workspace.cwd
120
- context.workspace.currentDir
121
- context.workspace.projectDir
122
- context.agent.name
123
- context.agent.id
124
- context.model.id
125
- context.model.displayName
126
- context.model.provider
127
- context.model.reasoningEffort
128
- context.permissionMode
129
- context.terminalWidth
130
- context.contextWindow.usedPercentage
131
- context.contextWindow.remainingPercentage
132
- context.cost.totalDurationMs
133
- context.cost.totalCostUsd
134
- context.reflection
135
- context.memfs
136
- context.backgroundAgents
137
- context.rawPayload // compatibility payload for advanced cases
134
+ ```tsx
135
+ render: ({ width, agent, model, row, chalk }) =>
136
+ row(chalk.dim("Press / for commands"), `${agent.name ?? "Letta"} \u00b7 ${model.displayName ?? ""}`, width),
138
137
  ```
139
138
 
140
- Prefer semantic fields over `rawPayload` unless migrating old command statuslines.
141
-
142
- ## Full-row layout
143
-
144
- New statuslines do not have a host left/right API. To create left/right visual alignment, do it inside the renderer:
139
+ Use `columns` for three or more evenly-spread segments:
145
140
 
146
141
  ```tsx
147
- return (
148
- <Box flexDirection="row">
149
- <Box flexGrow={1}>
150
- <Text>left content</Text>
151
- </Box>
152
- <Text>right content</Text>
153
- </Box>
154
- );
142
+ render: ({ width, columns }) => columns(["left", "middle", "right"], width),
155
143
  ```
156
144
 
157
145
  ## Reload behavior
@@ -162,4 +150,4 @@ After editing `~/.letta/mods/statusline.tsx`, tell the user to run:
162
150
  /reload
163
151
  ```
164
152
 
165
- The runtime tracks mod loading separately from “no custom statusline,” so a custom statusline should not flash back to the built-in default during reload.
153
+ The runtime tracks mod loading so a custom statusline does not flash back to the built-in default during reload.
@@ -1,17 +1,25 @@
1
1
  # Statusline Examples
2
2
 
3
- Use these as patterns, not mandatory templates. Keep the final mod focused on the user's request.
3
+ Use these as patterns, not mandatory templates. Keep the final mod focused on the user's request. All register at `order: 0` (the primary line) and return text composed with `row`/`columns`/`chalk`.
4
4
 
5
5
  ## Agent and model
6
6
 
7
7
  ```tsx
8
8
  export default function activate(letta) {
9
- if (!letta.capabilities.ui.customStatuslineRenderer) return;
10
-
11
- letta.ui.setStatuslineRenderer((context) => {
12
- const { Text } = context.components;
13
- return <Text>{context.agent.name ?? "Letta"} · {context.model.displayName ?? "no model"}</Text>;
9
+ if (!letta.capabilities.ui.panels) return;
10
+
11
+ const panel = letta.ui.openPanel({
12
+ id: "statusline",
13
+ order: 0,
14
+ render: ({ width, agent, model, row, chalk }) =>
15
+ row(
16
+ chalk.cyan(agent.name ?? "Letta"),
17
+ chalk.dim(model.displayName ?? "no model"),
18
+ width,
19
+ ),
14
20
  });
21
+
22
+ return () => panel.close();
15
23
  }
16
24
  ```
17
25
 
@@ -24,28 +32,35 @@ import { promisify } from "node:util";
24
32
  const execFileAsync = promisify(execFile);
25
33
 
26
34
  export default function activate(letta) {
27
- if (!letta.capabilities.ui.customStatuslineRenderer) return;
35
+ if (!letta.capabilities.ui.panels) return;
36
+
37
+ let branch = "";
38
+
39
+ const panel = letta.ui.openPanel({
40
+ id: "statusline",
41
+ order: 0,
42
+ render: ({ width, agent, row, chalk }) =>
43
+ row(branch ? chalk.green(`git ${branch}`) : (agent.name ?? "Letta"), "", width),
44
+ });
28
45
 
29
46
  const update = async () => {
30
47
  try {
31
48
  const { stdout } = await execFileAsync("git", ["branch", "--show-current"], {
32
49
  cwd: process.cwd(),
33
50
  });
34
- letta.ui.setStatus("branch", stdout.trim());
51
+ branch = stdout.trim();
35
52
  } catch {
36
- letta.ui.clearStatus("branch");
53
+ branch = "";
37
54
  }
55
+ panel.update();
38
56
  };
39
57
 
40
- letta.ui.setStatuslineRenderer((context) => {
41
- const { Text } = context.components;
42
- const branch = context.statuses.branch;
43
- return <Text>{branch ? `git ${branch}` : context.agent.name}</Text>;
44
- });
45
-
46
58
  void update();
47
59
  const timer = setInterval(update, 30_000);
48
- return () => clearInterval(timer);
60
+ return () => {
61
+ clearInterval(timer);
62
+ panel.close();
63
+ };
49
64
  }
50
65
  ```
51
66
 
@@ -53,21 +68,20 @@ export default function activate(letta) {
53
68
 
54
69
  ```tsx
55
70
  export default function activate(letta) {
56
- if (!letta.capabilities.ui.customStatuslineRenderer) return;
57
-
58
- letta.ui.setStatuslineRenderer((context) => {
59
- const { Box, Text } = context.components;
60
- const model = context.model.displayName ?? "no model";
61
-
62
- return (
63
- <Box flexDirection="row">
64
- <Box flexGrow={1}>
65
- <Text dimColor>Press / for commands</Text>
66
- </Box>
67
- <Text>{context.agent.name ?? "Letta"} · {model}</Text>
68
- </Box>
69
- );
71
+ if (!letta.capabilities.ui.panels) return;
72
+
73
+ const panel = letta.ui.openPanel({
74
+ id: "statusline",
75
+ order: 0,
76
+ render: ({ width, agent, model, row, chalk }) =>
77
+ row(
78
+ chalk.dim("Press / for commands"),
79
+ `${agent.name ?? "Letta"} \u00b7 ${model.displayName ?? "no model"}`,
80
+ width,
81
+ ),
70
82
  });
83
+
84
+ return () => panel.close();
71
85
  }
72
86
  ```
73
87
 
@@ -80,7 +94,15 @@ import { promisify } from "node:util";
80
94
  const execFileAsync = promisify(execFile);
81
95
 
82
96
  export default function activate(letta) {
83
- if (!letta.capabilities.ui.customStatuslineRenderer) return;
97
+ if (!letta.capabilities.ui.panels) return;
98
+
99
+ let pr = "";
100
+
101
+ const panel = letta.ui.openPanel({
102
+ id: "statusline",
103
+ order: 0,
104
+ render: ({ width, model, row }) => row(pr || (model.displayName ?? ""), "", width),
105
+ });
84
106
 
85
107
  const update = async () => {
86
108
  try {
@@ -89,21 +111,19 @@ export default function activate(letta) {
89
111
  ["pr", "view", "--json", "number,title", "--jq", "\"#\\(.number) \\(.title)\""],
90
112
  { cwd: process.cwd() },
91
113
  );
92
- const pr = stdout.trim();
93
- pr ? letta.ui.setStatus("pr", pr) : letta.ui.clearStatus("pr");
114
+ pr = stdout.trim();
94
115
  } catch {
95
- letta.ui.clearStatus("pr");
116
+ pr = "";
96
117
  }
118
+ panel.update();
97
119
  };
98
120
 
99
- letta.ui.setStatuslineRenderer((context) => {
100
- const { Text } = context.components;
101
- return <Text>{context.statuses.pr ?? context.model.displayName}</Text>;
102
- });
103
-
104
121
  void update();
105
122
  const timer = setInterval(update, 60_000);
106
- return () => clearInterval(timer);
123
+ return () => {
124
+ clearInterval(timer);
125
+ panel.close();
126
+ };
107
127
  }
108
128
  ```
109
129
 
@@ -116,26 +136,34 @@ import { promisify } from "node:util";
116
136
  const execFileAsync = promisify(execFile);
117
137
 
118
138
  export default function activate(letta) {
119
- if (!letta.capabilities.ui.customStatuslineRenderer) return;
139
+ if (!letta.capabilities.ui.panels) return;
140
+
141
+ let music = "";
142
+
143
+ const panel = letta.ui.openPanel({
144
+ id: "statusline",
145
+ order: 0,
146
+ render: ({ width, agent, row, chalk }) =>
147
+ row(music ? chalk.magenta(music) : (agent.name ?? "Letta"), "", width),
148
+ });
120
149
 
121
150
  const update = async () => {
122
151
  try {
123
- const script = 'tell application "Music" to if it is running then artist of current track & " - " & name of current track';
152
+ const script =
153
+ 'tell application "Music" to if it is running then artist of current track & " - " & name of current track';
124
154
  const { stdout } = await execFileAsync("osascript", ["-e", script]);
125
- const music = stdout.trim();
126
- music ? letta.ui.setStatus("music", music) : letta.ui.clearStatus("music");
155
+ music = stdout.trim();
127
156
  } catch {
128
- letta.ui.clearStatus("music");
157
+ music = "";
129
158
  }
159
+ panel.update();
130
160
  };
131
161
 
132
- letta.ui.setStatuslineRenderer((context) => {
133
- const { Text } = context.components;
134
- return <Text>{context.statuses.music ?? context.agent.name}</Text>;
135
- });
136
-
137
162
  void update();
138
163
  const timer = setInterval(update, 15_000);
139
- return () => clearInterval(timer);
164
+ return () => {
165
+ clearInterval(timer);
166
+ panel.close();
167
+ };
140
168
  }
141
169
  ```
@@ -1,6 +1,6 @@
1
1
  # Statusline Migration
2
2
 
3
- Use this reference when migrating legacy command statuslines, standalone `.sh` statusline scripts, or shell PS1 prompts.
3
+ Use this reference when migrating legacy command statuslines, standalone `.sh` statusline scripts, or shell PS1 prompts into `~/.letta/mods/statusline.tsx`.
4
4
 
5
5
  ## Legacy Letta command statusline
6
6
 
@@ -40,10 +40,10 @@ When migrating:
40
40
 
41
41
  - Preserve old config and referenced files unless the user explicitly asks to delete them.
42
42
  - If `command` references a `.sh` file, read it before writing the new mod.
43
- - Translate polling (`refreshIntervalMs`) to `setInterval`.
44
- - Translate direct command output into cached status plus synchronous rendering.
45
- - If the command output used `\x1e` to split left/right output, convert it to internal full-row layout with `Box`; do not create a new left/right API.
46
- - Treat old prompt customization separately. The new statusline controls the bottom row, not necessarily the input prompt.
43
+ - Translate polling (`refreshIntervalMs`) to `setInterval` + `panel.update()`.
44
+ - Translate direct command output into a cached closure variable plus synchronous rendering.
45
+ - If the command output used `\x1e` to split left/right output, convert it to `row(left, right, width)`; there is no separate left/right API.
46
+ - Treat old prompt customization separately. The statusline controls the primary row, not necessarily the input prompt.
47
47
 
48
48
  Old model:
49
49
 
@@ -59,17 +59,36 @@ import { promisify } from "node:util";
59
59
 
60
60
  const execFileAsync = promisify(execFile);
61
61
 
62
- const update = async () => {
63
- const { stdout } = await execFileAsync("git", ["branch", "--show-current"], {
64
- cwd: process.cwd(),
62
+ export default function activate(letta) {
63
+ if (!letta.capabilities.ui.panels) return;
64
+
65
+ let branch = "";
66
+
67
+ const panel = letta.ui.openPanel({
68
+ id: "statusline",
69
+ order: 0,
70
+ render: ({ width, row }) => row(branch, "", width),
65
71
  });
66
- letta.ui.setStatus("branch", stdout.trim());
67
- };
68
72
 
69
- letta.ui.setStatuslineRenderer((context) => {
70
- const { Text } = context.components;
71
- return <Text>{context.statuses.branch ?? ""}</Text>;
72
- });
73
+ const update = async () => {
74
+ try {
75
+ const { stdout } = await execFileAsync("git", ["branch", "--show-current"], {
76
+ cwd: process.cwd(),
77
+ });
78
+ branch = stdout.trim();
79
+ } catch {
80
+ branch = "";
81
+ }
82
+ panel.update();
83
+ };
84
+
85
+ void update();
86
+ const timer = setInterval(update, 30_000);
87
+ return () => {
88
+ clearInterval(timer);
89
+ panel.close();
90
+ };
91
+ }
73
92
  ```
74
93
 
75
94
  ## Standalone `.sh` file migration
@@ -79,11 +98,11 @@ If the user provides a `.sh` path:
79
98
  1. Read the script.
80
99
  2. Identify commands, expected stdin JSON, environment variables, and output shape.
81
100
  3. Port shell commands to async setup/update code.
82
- 4. Store results with `letta.ui.setStatus(key, value)`.
83
- 5. Render cached status synchronously.
84
- 6. Preserve graceful fallbacks for missing tools, not-a-git-repo, no PR, etc.
101
+ 4. Store results in closure variables.
102
+ 5. Render cached state synchronously and call `panel.update()` after each refresh.
103
+ 6. Preserve graceful fallbacks for missing tools, not-a-git-repo, no PR, etc. (return `""` to hide the line).
85
104
 
86
- If a script depends heavily on stdin JSON, use `context.rawPayload` as a temporary migration aid, but prefer semantic context fields for new code.
105
+ Map the script's stdin JSON to the render context's semantic fields (`agent`, `model`, `width`); compute anything else in setup/update code.
87
106
 
88
107
  ## Shell PS1 import
89
108
 
@@ -127,4 +146,4 @@ If no PS1 is found and the user did not provide other instructions, ask for one
127
146
  2. a description of what their prompt shows
128
147
  3. the current prompt output as it appears in their terminal
129
148
 
130
- Preserve colors where practical using display components. If the PS1 is too dynamic to port exactly, ask whether to approximate it or port specific commands.
149
+ Preserve colors where practical with `chalk`. If the PS1 is too dynamic to port exactly, ask whether to approximate it or port specific commands.