@letta-ai/letta-code 0.26.1 → 0.26.2

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.
@@ -0,0 +1,88 @@
1
+ ---
2
+ name: customizing-commands
3
+ description: Creates, edits, and enables Letta Code extension-provided slash commands. Use when the user asks to add a custom /command, slash command, command shortcut, SDK-backed command, or command-driven panel behavior.
4
+ ---
5
+
6
+ # Customizing Commands
7
+
8
+ Use this as the command-specific entrypoint for local extension slash commands. For broader extension work, recipes live in `../creating-extensions/references/commands.md`, `../creating-extensions/references/ui.md`, and `../creating-extensions/references/btw-command.md`.
9
+
10
+ Extension files live in:
11
+
12
+ ```text
13
+ ~/.letta/extensions/
14
+ ```
15
+
16
+ Use a focused file name, e.g. `~/.letta/extensions/review.ts` or `~/.letta/extensions/commands.ts`.
17
+
18
+ ## First decide whether a command is right
19
+
20
+ | User wants | Build |
21
+ | --- | --- |
22
+ | `/foo` sends a prompt or shows local output | Extension command |
23
+ | `/foo` starts a reusable agent workflow | Skill + thin extension command |
24
+ | Agent/model should autonomously call the capability | Extension tool, not a command |
25
+ | Command shows transient progress/results | Extension command + panel |
26
+
27
+ If the command is a durable workflow like `/goal`, put the workflow instructions in a skill and keep the extension command as a small launcher/prompt.
28
+
29
+ ## Workflow
30
+
31
+ 1. Inspect `~/.letta/extensions/` for related command files.
32
+ 2. Preserve unrelated extension code; create a focused new file if merging is messy.
33
+ 3. Register with `letta.commands.register()` and guard with `letta.capabilities.commands`.
34
+ 4. Return the unregister function, or a disposer that calls it plus any timer/panel cleanup.
35
+ 5. Tell the user the exact file path changed and to run `/reload`.
36
+
37
+ ## Default prompt command
38
+
39
+ ```ts
40
+ export default function activate(letta) {
41
+ if (!letta.capabilities.commands) return;
42
+
43
+ return letta.commands.register({
44
+ id: "review",
45
+ description: "Review current git changes",
46
+ args: "[focus]",
47
+ run(ctx) {
48
+ const focus = ctx.args.trim();
49
+ return {
50
+ type: "prompt",
51
+ content: focus
52
+ ? `Review current git changes. Focus on ${focus}.`
53
+ : "Review current git changes. Focus on correctness issues.",
54
+ systemReminder: true,
55
+ };
56
+ },
57
+ });
58
+ }
59
+ ```
60
+
61
+ ## Command result types
62
+
63
+ ```ts
64
+ type ExtensionCommandResult =
65
+ | { type: "prompt"; content: string; systemReminder?: boolean }
66
+ | { type: "output"; output: string; success?: boolean }
67
+ | { type: "handled" };
68
+ ```
69
+
70
+ - `prompt`: sends content to the agent. Use for normal slash shortcuts.
71
+ - `output`: prints local text and does not contact the agent.
72
+ - `handled`: command handled its own side effects/UI; common for panel commands.
73
+
74
+ ## Rules
75
+
76
+ - Command IDs omit the slash: `id: "review"`, not `"/review"`.
77
+ - Use lowercase slugs with letters, numbers, and hyphens.
78
+ - Do not register built-in command IDs.
79
+ - `runWhenBusy: true` commands must not return `prompt` while the main agent is busy; use SDK calls/panels and return `handled`.
80
+ - `showInTranscript: false` commands should usually return `handled`, not `prompt`.
81
+ - Do not import Letta Code app internals.
82
+ - Do not do surprising side effects on startup; extensions activate on app start and `/reload`.
83
+
84
+ ## More recipes
85
+
86
+ - Simple output command, panel command, SDK command: `../creating-extensions/references/commands.md`
87
+ - Panel/status UI patterns: `../creating-extensions/references/ui.md`
88
+ - Complete `/btw` side-question recipe: `../creating-extensions/references/btw-command.md`
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: customizing-statusline
3
+ description: Creates, edits, and migrates Letta Code statusline extensions. Use when handling the /statusline command or continuing work started by /statusline.
4
+ ---
5
+
6
+ # Customizing Statusline
7
+
8
+ Use this skill to create or update the global Letta Code statusline extension:
9
+
10
+ ```text
11
+ ~/.letta/extensions/statusline.tsx
12
+ ```
13
+
14
+ The statusline is a full-row idle renderer. Host UI can still temporarily preempt it for safety confirmations and transient hints.
15
+
16
+ ## Statusline ownership model
17
+
18
+ ```text
19
+ safety preemption
20
+ else transient host hint
21
+ else custom statusline extension
22
+ else built-in default statusline
23
+ ```
24
+
25
+ A custom statusline owns the whole idle row. Do not preserve legacy left/right split semantics in the new API.
26
+
27
+ ## Workflow
28
+
29
+ 1. Check whether `~/.letta/extensions/statusline.tsx` exists.
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.
32
+ 4. If the user asks to migrate, import a `.sh` file, or match a shell prompt, read `references/migration.md`.
33
+ 5. If API details or concrete patterns are needed, read `references/api.md` and `references/examples.md`.
34
+ 6. Guard statusline-specific behavior with `letta.capabilities.ui.customStatuslineRenderer` when writing new files.
35
+ 7. Edit `~/.letta/extensions/statusline.tsx`.
36
+ 8. Summarize the absolute file path changed and tell the user to run `/reload` unless the command can reload automatically.
37
+
38
+ ## Bare `/statusline` behavior
39
+
40
+ If the user ran `/statusline` without a specific request:
41
+
42
+ - If a custom statusline file exists, summarize what it appears to do and ask what they want to change.
43
+ - If no custom file exists, explain that Letta is using the built-in default statusline and offer focused next steps:
44
+ 1. start from the default Letta statusline
45
+ 2. add project info like git branch, worktree, or PR
46
+ 3. migrate an existing legacy statusline `.sh` file
47
+ 4. match shell prompt / PS1
48
+ 5. describe a custom statusline in their own words
49
+
50
+ Keep this conversational. Do not build a menu UI unless the product command explicitly asks for one.
51
+
52
+ ## Rules
53
+
54
+ - Global-only for now. Do not create project extensions.
55
+ - Keep the extension single-file for MVP.
56
+ - Do not assume extra npm packages are available.
57
+ - Do not use relative multi-file imports yet.
58
+ - Keep renderers synchronous. Do not shell, fetch, or await inside render.
59
+ - Do async work in setup code, intervals, subscriptions, or status providers.
60
+ - Use `letta.ui.setStatus` for data and `setStatuslineRenderer` for drawing that data.
61
+ - Guard optional APIs with `letta.capabilities.ui.statusValues` and `letta.capabilities.ui.customStatuslineRenderer` in new files.
62
+ - Return a disposer that clears timers/subscriptions.
63
+ - Preserve existing extension code unless the user asks to reset.
64
+ - Do not delete legacy command statusline files or settings unless the user explicitly asks.
65
+
66
+ ## Useful references
67
+
68
+ - `references/api.md` - extension API, render context, lifecycle rules
69
+ - `references/examples.md` - common statusline patterns
70
+ - `references/migration.md` - legacy command `.sh` and PS1 migration
@@ -0,0 +1,168 @@
1
+ # Statusline Extension API
2
+
3
+ Use this reference when creating or editing `~/.letta/extensions/statusline.tsx`.
4
+
5
+ ## Location
6
+
7
+ ```text
8
+ ~/.letta/extensions/statusline.tsx
9
+ ```
10
+
11
+ This is a trusted, user-owned global extension file. Project extensions are intentionally unsupported for now.
12
+
13
+ ## Activation
14
+
15
+ Export a default function or named `activate` function:
16
+
17
+ ```tsx
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>;
24
+ });
25
+ }
26
+ ```
27
+
28
+ ## API
29
+
30
+ ```ts
31
+ letta.getContext(): StatuslineRenderContext
32
+
33
+ letta.capabilities.ui.statusValues: boolean
34
+ letta.capabilities.ui.customStatuslineRenderer: boolean
35
+
36
+ letta.ui.setStatus(key: string, value: string | null | undefined | ((context) => string | null)): void
37
+ letta.ui.clearStatus(key: string): void
38
+ letta.ui.setStatuslineRenderer(renderer: StatuslineRenderer | ((context) => ReactNode | null)): void
39
+ ```
40
+
41
+ `setStatus` stores named string values. Renderers read evaluated values from `context.statuses`.
42
+
43
+ ```tsx
44
+ letta.ui.setStatus("branch", "main");
45
+
46
+ letta.ui.setStatuslineRenderer((context) => {
47
+ const { Text } = context.components;
48
+ return <Text>{context.statuses.branch}</Text>;
49
+ });
50
+ ```
51
+
52
+ ## Renderer rules
53
+
54
+ - Renderer owns the entire idle bottom row.
55
+ - Renderer must be synchronous.
56
+ - Do not run shell commands, network requests, file reads, or awaits inside render.
57
+ - Do async work in setup code or intervals, store results with `setStatus`, then render `context.statuses`.
58
+ - Return `null` only when intentionally rendering nothing.
59
+
60
+ ## Async state pattern
61
+
62
+ Use Node/Bun APIs directly from the trusted extension file. Do not assume helper methods like `letta.shell` exist.
63
+
64
+ ```tsx
65
+ import { execFile } from "node:child_process";
66
+ import { promisify } from "node:util";
67
+
68
+ const execFileAsync = promisify(execFile);
69
+
70
+ export default function activate(letta) {
71
+ if (!letta.capabilities.ui.customStatuslineRenderer) return;
72
+
73
+ const update = async () => {
74
+ try {
75
+ const context = letta.getContext();
76
+ const { stdout } = await execFileAsync("git", ["branch", "--show-current"], {
77
+ cwd: context.workspace.currentDir,
78
+ });
79
+ if (letta.capabilities.ui.statusValues) {
80
+ letta.ui.setStatus("branch", stdout.trim());
81
+ }
82
+ } catch {
83
+ if (letta.capabilities.ui.statusValues) {
84
+ letta.ui.clearStatus("branch");
85
+ }
86
+ }
87
+ };
88
+
89
+ letta.ui.setStatuslineRenderer((context) => {
90
+ const { Text } = context.components;
91
+ const branch = context.statuses.branch;
92
+ return <Text>{branch ? `branch ${branch}` : context.agent.name}</Text>;
93
+ });
94
+
95
+ void update();
96
+ const timer = setInterval(update, 30_000);
97
+
98
+ return () => {
99
+ clearInterval(timer);
100
+ if (letta.capabilities.ui.statusValues) {
101
+ letta.ui.clearStatus("branch");
102
+ }
103
+ };
104
+ }
105
+ ```
106
+
107
+ ## Context fields
108
+
109
+ The app statusline render context source types live near:
110
+
111
+ ```text
112
+ src/cli/display/statusline/types.ts
113
+ src/cli/display/statusline/context.ts
114
+ ```
115
+
116
+ Common fields:
117
+
118
+ ```ts
119
+ context.components // Display components such as Text, Box, Spacer
120
+ context.statuses // evaluated extension status strings
121
+ context.app.version
122
+ context.workspace.cwd
123
+ context.workspace.currentDir
124
+ context.workspace.projectDir
125
+ context.agent.name
126
+ context.agent.id
127
+ context.model.id
128
+ context.model.displayName
129
+ context.model.provider
130
+ context.model.reasoningEffort
131
+ context.permissionMode
132
+ context.terminalWidth
133
+ context.contextWindow.usedPercentage
134
+ context.contextWindow.remainingPercentage
135
+ context.cost.totalDurationMs
136
+ context.cost.totalCostUsd
137
+ context.reflection
138
+ context.memfs
139
+ context.backgroundAgents
140
+ context.rawPayload // compatibility payload for advanced cases
141
+ ```
142
+
143
+ Prefer semantic fields over `rawPayload` unless migrating old command statuslines.
144
+
145
+ ## Full-row layout
146
+
147
+ New statuslines do not have a host left/right API. To create left/right visual alignment, do it inside the renderer:
148
+
149
+ ```tsx
150
+ return (
151
+ <Box flexDirection="row">
152
+ <Box flexGrow={1}>
153
+ <Text>left content</Text>
154
+ </Box>
155
+ <Text>right content</Text>
156
+ </Box>
157
+ );
158
+ ```
159
+
160
+ ## Reload behavior
161
+
162
+ After editing `~/.letta/extensions/statusline.tsx`, tell the user to run:
163
+
164
+ ```text
165
+ /reload
166
+ ```
167
+
168
+ The runtime tracks extension loading separately from “no custom statusline,” so a custom statusline should not flash back to the built-in default during reload.
@@ -0,0 +1,143 @@
1
+ # Statusline Examples
2
+
3
+ Use these as patterns, not mandatory templates. Keep the final extension focused on the user's request.
4
+
5
+ ## Agent and model
6
+
7
+ ```tsx
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>;
14
+ });
15
+ }
16
+ ```
17
+
18
+ ## Git branch with fallback
19
+
20
+ ```tsx
21
+ import { execFile } from "node:child_process";
22
+ import { promisify } from "node:util";
23
+
24
+ const execFileAsync = promisify(execFile);
25
+
26
+ export default function activate(letta) {
27
+ if (!letta.capabilities.ui.customStatuslineRenderer) return;
28
+
29
+ const update = async () => {
30
+ try {
31
+ const context = letta.getContext();
32
+ const { stdout } = await execFileAsync("git", ["branch", "--show-current"], {
33
+ cwd: context.workspace.currentDir,
34
+ });
35
+ letta.ui.setStatus("branch", stdout.trim());
36
+ } catch {
37
+ letta.ui.clearStatus("branch");
38
+ }
39
+ };
40
+
41
+ letta.ui.setStatuslineRenderer((context) => {
42
+ const { Text } = context.components;
43
+ const branch = context.statuses.branch;
44
+ return <Text>{branch ? `git ${branch}` : context.agent.name}</Text>;
45
+ });
46
+
47
+ void update();
48
+ const timer = setInterval(update, 30_000);
49
+ return () => clearInterval(timer);
50
+ }
51
+ ```
52
+
53
+ ## Full row with internal right alignment
54
+
55
+ ```tsx
56
+ export default function activate(letta) {
57
+ if (!letta.capabilities.ui.customStatuslineRenderer) return;
58
+
59
+ letta.ui.setStatuslineRenderer((context) => {
60
+ const { Box, Text } = context.components;
61
+ const model = context.model.displayName ?? "no model";
62
+
63
+ return (
64
+ <Box flexDirection="row">
65
+ <Box flexGrow={1}>
66
+ <Text dimColor>Press / for commands</Text>
67
+ </Box>
68
+ <Text>{context.agent.name ?? "Letta"} · {model}</Text>
69
+ </Box>
70
+ );
71
+ });
72
+ }
73
+ ```
74
+
75
+ ## GitHub PR number via `gh`
76
+
77
+ ```tsx
78
+ import { execFile } from "node:child_process";
79
+ import { promisify } from "node:util";
80
+
81
+ const execFileAsync = promisify(execFile);
82
+
83
+ export default function activate(letta) {
84
+ if (!letta.capabilities.ui.customStatuslineRenderer) return;
85
+
86
+ const update = async () => {
87
+ try {
88
+ const context = letta.getContext();
89
+ const { stdout } = await execFileAsync(
90
+ "gh",
91
+ ["pr", "view", "--json", "number,title", "--jq", "\"#\\(.number) \\(.title)\""],
92
+ { cwd: context.workspace.currentDir },
93
+ );
94
+ const pr = stdout.trim();
95
+ pr ? letta.ui.setStatus("pr", pr) : letta.ui.clearStatus("pr");
96
+ } catch {
97
+ letta.ui.clearStatus("pr");
98
+ }
99
+ };
100
+
101
+ letta.ui.setStatuslineRenderer((context) => {
102
+ const { Text } = context.components;
103
+ return <Text>{context.statuses.pr ?? context.model.displayName}</Text>;
104
+ });
105
+
106
+ void update();
107
+ const timer = setInterval(update, 60_000);
108
+ return () => clearInterval(timer);
109
+ }
110
+ ```
111
+
112
+ ## macOS currently playing track
113
+
114
+ ```tsx
115
+ import { execFile } from "node:child_process";
116
+ import { promisify } from "node:util";
117
+
118
+ const execFileAsync = promisify(execFile);
119
+
120
+ export default function activate(letta) {
121
+ if (!letta.capabilities.ui.customStatuslineRenderer) return;
122
+
123
+ const update = async () => {
124
+ try {
125
+ const script = 'tell application "Music" to if it is running then artist of current track & " - " & name of current track';
126
+ const { stdout } = await execFileAsync("osascript", ["-e", script]);
127
+ const music = stdout.trim();
128
+ music ? letta.ui.setStatus("music", music) : letta.ui.clearStatus("music");
129
+ } catch {
130
+ letta.ui.clearStatus("music");
131
+ }
132
+ };
133
+
134
+ letta.ui.setStatuslineRenderer((context) => {
135
+ const { Text } = context.components;
136
+ return <Text>{context.statuses.music ?? context.agent.name}</Text>;
137
+ });
138
+
139
+ void update();
140
+ const timer = setInterval(update, 15_000);
141
+ return () => clearInterval(timer);
142
+ }
143
+ ```
@@ -0,0 +1,131 @@
1
+ # Statusline Migration
2
+
3
+ Use this reference when migrating legacy command statuslines, standalone `.sh` statusline scripts, or shell PS1 prompts.
4
+
5
+ ## Legacy Letta command statusline
6
+
7
+ Inspect these files for old config:
8
+
9
+ ```text
10
+ ~/.letta/settings.json
11
+ <project>/.letta/settings.json
12
+ <project>/.letta/settings.local.json
13
+ ```
14
+
15
+ Look for either shape:
16
+
17
+ ```json
18
+ {
19
+ "statusLine": {
20
+ "type": "command",
21
+ "command": "..."
22
+ }
23
+ }
24
+ ```
25
+
26
+ ```json
27
+ {
28
+ "statusLine": {
29
+ "command": "...",
30
+ "refreshIntervalMs": 30000,
31
+ "timeout": 5000,
32
+ "debounceMs": 300,
33
+ "padding": 0,
34
+ "prompt": ">"
35
+ }
36
+ }
37
+ ```
38
+
39
+ When migrating:
40
+
41
+ - Preserve old config and referenced files unless the user explicitly asks to delete them.
42
+ - If `command` references a `.sh` file, read it before writing the new extension.
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.
47
+
48
+ Old model:
49
+
50
+ ```sh
51
+ echo "$(git branch --show-current)"
52
+ ```
53
+
54
+ New model:
55
+
56
+ ```tsx
57
+ import { execFile } from "node:child_process";
58
+ import { promisify } from "node:util";
59
+
60
+ const execFileAsync = promisify(execFile);
61
+
62
+ const update = async () => {
63
+ const context = letta.getContext();
64
+ const { stdout } = await execFileAsync("git", ["branch", "--show-current"], {
65
+ cwd: context.workspace.currentDir,
66
+ });
67
+ letta.ui.setStatus("branch", stdout.trim());
68
+ };
69
+
70
+ letta.ui.setStatuslineRenderer((context) => {
71
+ const { Text } = context.components;
72
+ return <Text>{context.statuses.branch ?? ""}</Text>;
73
+ });
74
+ ```
75
+
76
+ ## Standalone `.sh` file migration
77
+
78
+ If the user provides a `.sh` path:
79
+
80
+ 1. Read the script.
81
+ 2. Identify commands, expected stdin JSON, environment variables, and output shape.
82
+ 3. Port shell commands to async setup/update code.
83
+ 4. Store results with `letta.ui.setStatus(key, value)`.
84
+ 5. Render cached status synchronously.
85
+ 6. Preserve graceful fallbacks for missing tools, not-a-git-repo, no PR, etc.
86
+
87
+ If a script depends heavily on stdin JSON, use `context.rawPayload` as a temporary migration aid, but prefer semantic context fields for new code.
88
+
89
+ ## Shell PS1 import
90
+
91
+ If the user asks to match their shell prompt, inspect shell config files in this order:
92
+
93
+ ```text
94
+ ~/.zshrc
95
+ ~/.bashrc
96
+ ~/.bash_profile
97
+ ~/.profile
98
+ ```
99
+
100
+ Extract PS1 with:
101
+
102
+ ```js
103
+ /(?:^|\n)\s*(?:export\s+)?PS1\s*=\s*["']([^"']+)["']/m
104
+ ```
105
+
106
+ Map common escapes:
107
+
108
+ ```text
109
+ \u -> username
110
+ \h -> short hostname
111
+ \H -> hostname
112
+ \w -> current working directory
113
+ \W -> basename(current working directory)
114
+ \$ -> prompt character, usually remove if trailing
115
+ \n -> newline
116
+ \t -> HH:MM:SS
117
+ \d -> date like Tue May 23
118
+ \@ -> 12-hour time
119
+ \# -> command number, usually omit unless requested
120
+ \! -> history number, usually omit unless requested
121
+ ```
122
+
123
+ If the imported prompt ends with `$`, `>`, or similar prompt chars, remove that trailing prompt marker. The statusline is not the input prompt.
124
+
125
+ If no PS1 is found and the user did not provide other instructions, ask for one of:
126
+
127
+ 1. the output of `echo $PS1`
128
+ 2. a description of what their prompt shows
129
+ 3. the current prompt output as it appears in their terminal
130
+
131
+ 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.