@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.
- package/README.md +1 -1
- package/letta.js +318352 -144445
- package/package.json +5 -4
- package/scripts/check-test-coverage.cjs +131 -0
- package/scripts/check.js +1 -0
- package/scripts/run-unit-tests.cjs +2 -0
- package/skills/creating-extensions/SKILL.md +87 -0
- package/skills/creating-extensions/references/btw-command.md +90 -0
- package/skills/creating-extensions/references/commands.md +114 -0
- package/skills/creating-extensions/references/tools.md +115 -0
- package/skills/creating-extensions/references/ui.md +65 -0
- package/skills/customizing-commands/SKILL.md +88 -0
- package/skills/customizing-statusline/SKILL.md +70 -0
- package/skills/customizing-statusline/references/api.md +168 -0
- package/skills/customizing-statusline/references/examples.md +143 -0
- package/skills/customizing-statusline/references/migration.md +131 -0
- package/skills/letta-help/SKILL.md +219 -0
|
@@ -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.
|