@letta-ai/letta-code 0.27.17 → 0.27.19

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.
Files changed (30) hide show
  1. package/README.md +7 -0
  2. package/dist/app-server-client.js +11 -1
  3. package/dist/app-server-client.js.map +3 -3
  4. package/dist/types/app-server-client.d.ts +4 -1
  5. package/dist/types/app-server-client.d.ts.map +1 -1
  6. package/dist/types/types/protocol.d.ts +2 -1
  7. package/dist/types/types/protocol.d.ts.map +1 -1
  8. package/dist/types/types/protocol_v2.d.ts +17 -3
  9. package/dist/types/types/protocol_v2.d.ts.map +1 -1
  10. package/docs/examples/mods/README.md +60 -0
  11. package/docs/examples/mods/learning/memory-citations.env.json +165 -0
  12. package/docs/examples/mods/learning/uv-pip-install.env.json +199 -0
  13. package/docs/examples/mods/memory-citations.ts +347 -0
  14. package/letta.js +125665 -122383
  15. package/package.json +4 -2
  16. package/scripts/mod-learning/learn-mod.ts +306 -0
  17. package/skills/{context_doctor → context-doctor}/SKILL.md +1 -1
  18. package/skills/creating-mods/SKILL.md +10 -7
  19. package/skills/creating-mods/references/architecture.md +15 -4
  20. package/skills/creating-mods/references/commands.md +2 -1
  21. package/skills/creating-mods/references/events.md +141 -10
  22. package/skills/creating-mods/references/plan-mode.md +1 -1
  23. package/skills/creating-mods/references/ui.md +61 -23
  24. package/skills/customizing-statusline/SKILL.md +13 -14
  25. package/skills/customizing-statusline/references/api.md +74 -86
  26. package/skills/customizing-statusline/references/examples.md +79 -51
  27. package/skills/customizing-statusline/references/migration.md +38 -19
  28. package/skills/generating-mod-envs/SKILL.md +130 -0
  29. package/skills/generating-mod-envs/assets/mod-learning-env.template.json +57 -0
  30. package/skills/generating-mod-envs/scripts/validate-mod-env.ts +261 -0
@@ -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,92 @@ 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`
37
+
38
+ `order` is a signed coordinate around the input:
39
+
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.
39
43
 
40
- ## Status values
44
+ A panel whose `render` is empty (`""`, `[]`, or only blank lines) is hidden.
45
+
46
+ ### Render context
41
47
 
42
48
  ```ts
43
- if (letta.capabilities.ui.statusValues) {
44
- letta.ui.setStatus("branch", "main");
45
- }
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[]
46
57
  ```
47
58
 
48
- Clear status values in disposers if they are owned by timers, events, or external state:
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
+
63
+ ### Commands that open panels
64
+
65
+ If a command's `run()` opens a panel, guard the command **registration** on `letta.capabilities.ui.panels` — not just the `openPanel` call:
49
66
 
50
67
  ```ts
51
- return () => {
52
- letta.ui.clearStatus("branch");
53
- };
68
+ export default function activate(letta) {
69
+ if (!letta.capabilities.commands) return;
70
+ if (!letta.capabilities.ui.panels) return; // panel-only command: skip where panels are unsupported
71
+
72
+ return letta.commands.register({
73
+ id: "mycommand",
74
+ description: "…",
75
+ run() {
76
+ const panel = letta.ui.openPanel({ /* … */ });
77
+ // …
78
+ return { type: "handled" };
79
+ },
80
+ });
81
+ }
54
82
  ```
55
83
 
84
+ Guarding only the `openPanel` call is not enough. The desktop listener has `commands: true` but `ui.panels: false`, so a command registered there is advertised in the command picker while its panel work no-ops — the user sees a command that runs and does nothing. Gating the registration keeps panel-only commands out of hosts that can't render them.
85
+
56
86
  ## Timers and cleanup
57
87
 
58
88
  ```ts
59
89
  export default function activate(letta) {
60
- if (!letta.capabilities.ui.statusValues) return;
90
+ if (!letta.capabilities.ui.panels) return;
91
+
92
+ let clock = new Date().toLocaleTimeString();
93
+ const panel = letta.ui.openPanel({
94
+ id: "clock",
95
+ order: 100,
96
+ render: ({ width, row }) => row("clock", clock, width),
97
+ });
61
98
 
62
- const update = () => letta.ui.setStatus("clock", new Date().toLocaleTimeString());
63
- update();
64
- const timer = setInterval(update, 30_000);
99
+ const timer = setInterval(() => {
100
+ clock = new Date().toLocaleTimeString();
101
+ panel.update();
102
+ }, 30_000);
65
103
 
66
104
  return () => {
67
105
  clearInterval(timer);
68
- letta.ui.clearStatus("clock");
106
+ panel.close();
69
107
  };
70
108
  }
71
109
  ```
@@ -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
@@ -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
  ```