@aliou/pi-dev-kit 0.4.9 → 0.6.0

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,166 @@
1
+ # Components
2
+
3
+ TUI components render custom UI in the terminal. They are used in `ctx.ui.custom()`, `renderResult`, and other display contexts.
4
+
5
+ ## Component Interface
6
+
7
+ ```typescript
8
+ import type { Component, Theme } from "@mariozechner/pi-tui";
9
+
10
+ class MyComponent implements Component {
11
+ render(maxWidth: number, maxHeight: number): string {
12
+ return "Hello from my component";
13
+ }
14
+
15
+ // Optional: handle keyboard input
16
+ handleInput?(key: string): void;
17
+ }
18
+ ```
19
+
20
+ `render` is called whenever the TUI needs to repaint. Return a string (can include ANSI codes via theme helpers). `maxWidth` and `maxHeight` are the available terminal dimensions.
21
+
22
+ ## Available Components from pi-tui
23
+
24
+ Before creating custom components, check if an existing one fits your need:
25
+
26
+ | Component | Description |
27
+ |---|---|
28
+ | `Text` | Styled text with wrapping |
29
+ | `Box` | Container with borders and padding |
30
+ | `Container` | Vertical/horizontal layout |
31
+ | `Spacer` | Empty space |
32
+ | `Input` | Text input field |
33
+ | `Editor` | Multi-line text editor |
34
+ | `SelectList` | Scrollable selection list |
35
+ | `SettingsList` | Key-value settings display |
36
+ | `Loader` | Loading spinner |
37
+ | `CancellableLoader` | Loader with cancel support |
38
+ | `Markdown` | Markdown rendering |
39
+ | `Image` | Image rendering (kitty/sixel protocol) |
40
+ | `TruncatedText` | Text with line limit and expand/collapse |
41
+
42
+ Import from `@mariozechner/pi-tui`:
43
+
44
+ ```typescript
45
+ import { Text, Box, Container, SelectList } from "@mariozechner/pi-tui";
46
+ ```
47
+
48
+ ## Utility Components from pi-coding-agent
49
+
50
+ These are higher-level components for common extension patterns:
51
+
52
+ | Component | Description |
53
+ |---|---|
54
+ | `DynamicBorder` | Border that adjusts to content width |
55
+ | `BorderedLoader` | Loader inside a bordered box with optional cancel |
56
+ | `ToolExecutionComponent` | Standard tool execution display |
57
+
58
+ Import from `@mariozechner/pi-coding-agent`:
59
+
60
+ ```typescript
61
+ import { DynamicBorder, BorderedLoader } from "@mariozechner/pi-coding-agent";
62
+ ```
63
+
64
+ ## Using ctx.ui.custom()
65
+
66
+ `custom()` displays a full-screen component and returns when the component calls `done()`.
67
+
68
+ ```typescript
69
+ const result = await ctx.ui.custom<string>((tui, theme, kb, done) => {
70
+ return new MyPickerComponent(theme, items, (selected) => done(selected));
71
+ });
72
+ ```
73
+
74
+ Parameters passed to the factory:
75
+ - `tui`: The TUI instance (rarely needed directly).
76
+ - `theme`: Current theme for styling.
77
+ - `kb`: Keybinding configuration.
78
+ - `done(value)`: Call to close the component and return the value.
79
+
80
+ The generic type (`<string>` above) is the type of value passed to `done()`.
81
+
82
+ Mode behavior:
83
+ - Interactive: returns the value passed to `done(value)`.
84
+ - RPC: returns `undefined` (by design; no local TUI).
85
+ - Print: returns `undefined`.
86
+
87
+ Important: `undefined` is ambiguous. In Interactive mode you can also produce it yourself by calling `done(undefined)`. If you need to detect RPC fallback, use explicit non-undefined sentinels for interactive close paths (`null`, `"closed"`, `false`, etc.).
88
+
89
+ See `references/modes.md` for the three-tier pattern.
90
+
91
+ ## Theme Styling
92
+
93
+ All render functions receive a `theme` object for consistent styling:
94
+
95
+ ```typescript
96
+ // Foreground colors
97
+ theme.fg("toolTitle", text) // Tool names
98
+ theme.fg("accent", text) // Highlights
99
+ theme.fg("success", text) // Green
100
+ theme.fg("error", text) // Red
101
+ theme.fg("warning", text) // Yellow
102
+ theme.fg("muted", text) // Secondary text
103
+ theme.fg("dim", text) // Tertiary text
104
+
105
+ // Text styles
106
+ theme.bold(text)
107
+ theme.italic(text)
108
+ theme.strikethrough(text)
109
+ ```
110
+
111
+ ## `renderResult` is not a Component
112
+
113
+ `renderResult` is a plain render function. It returns a string (or `undefined`) and is not an interactive `Component` class.
114
+
115
+ ```typescript
116
+ renderResult(result, { expanded, isPartial }, theme) {
117
+ if (isPartial) return theme.fg("muted", "Loading...");
118
+
119
+ const items = result.details?.items ?? [];
120
+ const visible = expanded ? items : items.slice(0, 5);
121
+ return [
122
+ theme.fg("success", `Found ${items.length} results`),
123
+ ...visible.map((item: string) => ` ${theme.fg("accent", item)}`),
124
+ ].join("\n");
125
+ },
126
+ ```
127
+
128
+ For full `renderCall`/`renderResult` formatting rules, follow `references/tools.md` (Tool UI Rendering Guidelines + consistency contract).
129
+
130
+ ## Keyboard Handling in custom()
131
+
132
+ Interactive components handle keyboard input through `handleInput`:
133
+
134
+ ```typescript
135
+ class MyComponent implements Component {
136
+ private done: (value: string | null) => void;
137
+
138
+ constructor(done: (value: string | null) => void) {
139
+ this.done = done;
140
+ }
141
+
142
+ handleInput(key: string) {
143
+ if (key === "escape" || key === "q") {
144
+ this.done(null); // Cancel (explicit sentinel)
145
+ }
146
+ if (key === "return") {
147
+ this.done("selected"); // Confirm
148
+ }
149
+ }
150
+
151
+ render(maxWidth: number, maxHeight: number): string {
152
+ return "Press Enter to confirm, Esc to cancel";
153
+ }
154
+ }
155
+ ```
156
+
157
+ ## Code Highlighting
158
+
159
+ For displaying code in renderers:
160
+
161
+ ```typescript
162
+ import { highlightCode, getLanguageFromPath } from "@mariozechner/pi-coding-agent";
163
+
164
+ const lang = getLanguageFromPath("/path/to/file.ts"); // "typescript"
165
+ const highlighted = highlightCode(code, lang, theme);
166
+ ```
@@ -0,0 +1,54 @@
1
+ # Documentation
2
+
3
+ Every published extension should have a README that explains what it does, how to set it up, and what it provides.
4
+
5
+ ## README Template
6
+
7
+ ```markdown
8
+ # pi-my-extension
9
+
10
+ Brief description of the extension.
11
+
12
+ ## Setup
13
+
14
+ \`\`\`bash
15
+ pi install @scope/pi-my-extension
16
+ \`\`\`
17
+
18
+ ### Environment Variables
19
+
20
+ | Variable | Required | Description |
21
+ |---|---|---|
22
+ | `MY_API_KEY` | Yes | API key from [provider](https://...) |
23
+
24
+ ## Tools
25
+
26
+ | Tool | Description |
27
+ |---|---|
28
+ | `my_tool` | What it does |
29
+
30
+ ## Commands
31
+
32
+ | Command | Description |
33
+ |---|---|
34
+ | `/my-command` | What it does |
35
+
36
+ ## Providers
37
+
38
+ | Provider | Models |
39
+ |---|---|
40
+ | `my-provider` | model-a, model-b |
41
+ ```
42
+
43
+ ## What to Document
44
+
45
+ - **Installation**: `pi install` command.
46
+ - **Environment variables**: Every required and optional env var, with links to where to get them.
47
+ - **Tools**: Name and description of each registered tool. Include example usage if non-obvious.
48
+ - **Commands**: Name and description of each registered command.
49
+ - **Providers**: Provider name and available models (if the extension registers a provider).
50
+ - **Limitations**: Known limitations, unsupported modes, or missing features.
51
+
52
+ ## Changelog
53
+
54
+ If using changesets, the CHANGELOG.md is generated automatically. Each changeset entry should describe what changed from the user's perspective, not implementation details.
@@ -0,0 +1,244 @@
1
+ # Hooks (Events)
2
+
3
+ Hooks let extensions react to lifecycle events. They are registered with `pi.on(eventName, handler)`.
4
+
5
+ ## Event Reference
6
+
7
+ ### Session Events
8
+
9
+ | Event | When | Can Cancel | Payload |
10
+ |---|---|---|---|
11
+ | `session_start` | New session created | No | `{}` |
12
+ | `session_switch` | Switched to different session | No | `{ reason: "new" \| "switch" \| "fork" }` |
13
+ | `session_before_switch` | Before switching sessions | Yes (`{ cancel: true }`) | `{ reason: "new" \| "switch" \| "fork" }` |
14
+ | `session_before_fork` | Before forking a session | Yes (`{ cancel: true }`) | `{}` |
15
+ | `session_fork` | After session was forked | No | `{}` |
16
+ | `session_shutdown` | Pi is shutting down | No | `{}` |
17
+ | `session_before_compact` | Before compaction | Yes (return custom summary string) | `{ summary: string }` |
18
+
19
+ ### Agent Events
20
+
21
+ | Event | When | Payload |
22
+ |---|---|---|
23
+ | `before_agent_start` | Before agent turn starts | `{}` |
24
+ | `agent_start` | Agent turn started | `{}` |
25
+ | `turn_start` | Turn begins processing | `{}` |
26
+ | `turn_end` | Turn finishes processing | `{}` |
27
+ | `model_select` | User changed the model | `{ model: string }` |
28
+
29
+ ### Tool Events
30
+
31
+ | Event | When | Can Block | Payload |
32
+ |---|---|---|---|
33
+ | `tool_call` | Before a tool executes | Yes (`{ block: true, reason }`) | `{ toolName, toolCallId, input }` |
34
+
35
+ ### Input Events
36
+
37
+ | Event | When | Can Transform | Payload |
38
+ |---|---|---|---|
39
+ | `input` | User submitted a message | Yes (return transformed text) | `{ text: string }` |
40
+
41
+ ### Bash Events
42
+
43
+ | Event | When | Can Modify | Payload |
44
+ |---|---|---|---|
45
+ | `user_bash` | Before bash command runs | Yes (return modified command/cwd/env) | `{ command, cwd }` |
46
+
47
+ ## Handler Signature
48
+
49
+ ```typescript
50
+ pi.on("event_name", async (event, ctx) => {
51
+ // event: event-specific payload
52
+ // ctx: ExtensionContext (hasUI, ui methods, cwd, model, etc.)
53
+ });
54
+ ```
55
+
56
+ The handler receives the event payload and an `ExtensionContext`. The context provides access to UI methods, the current working directory, model info, and more.
57
+
58
+ ## Blocking and Cancelling
59
+
60
+ Some events let you prevent the default behavior by returning an object.
61
+
62
+ ### Blocking Tool Calls
63
+
64
+ ```typescript
65
+ pi.on("tool_call", async (event, ctx) => {
66
+ if (event.toolName === "bash" && event.input.command.includes("rm -rf /")) {
67
+ // Check ctx.hasUI before prompting -- see references/modes.md
68
+ if (!ctx.hasUI) {
69
+ return { block: true, reason: "Blocked: dangerous command (no UI to confirm)" };
70
+ }
71
+
72
+ const confirmed = await ctx.ui.confirm(
73
+ "Dangerous Command",
74
+ `Allow: ${event.input.command}?`
75
+ );
76
+ if (!confirmed) {
77
+ return { block: true, reason: "Blocked by user" };
78
+ }
79
+ }
80
+ return undefined; // Allow the tool call
81
+ });
82
+ ```
83
+
84
+ ### Cancelling Session Operations
85
+
86
+ ```typescript
87
+ pi.on("session_before_switch", async (event, ctx) => {
88
+ if (event.reason === "new" && ctx.hasUI) {
89
+ const confirmed = await ctx.ui.confirm("Clear session?", "All messages will be lost.");
90
+ if (!confirmed) {
91
+ return { cancel: true };
92
+ }
93
+ }
94
+ });
95
+ ```
96
+
97
+ ### Custom Compaction
98
+
99
+ ```typescript
100
+ pi.on("session_before_compact", async (event, ctx) => {
101
+ // Return a custom summary string to replace the default compaction
102
+ return `Custom summary: ${event.summary.slice(0, 200)}...`;
103
+ });
104
+ ```
105
+
106
+ ## Transforming Input
107
+
108
+ ```typescript
109
+ pi.on("input", async (event, ctx) => {
110
+ if (event.text.startsWith("!")) {
111
+ return event.text.slice(1).toUpperCase();
112
+ }
113
+ return undefined; // No transformation
114
+ });
115
+ ```
116
+
117
+ ## Modifying Bash Commands
118
+
119
+ ```typescript
120
+ pi.on("user_bash", async (event, ctx) => {
121
+ return {
122
+ command: event.command,
123
+ cwd: "/sandboxed/directory",
124
+ env: { ...process.env, SANDBOX: "true" },
125
+ };
126
+ });
127
+ ```
128
+
129
+ ## before_agent_start
130
+
131
+ This event fires before each agent turn. It is commonly used to modify the system prompt.
132
+
133
+ The handler receives a `BeforeAgentStartEvent` with a `systemPrompt` field containing the current prompt. Return a `{ systemPrompt }` object to replace it. If multiple extensions return a modified prompt, they are chained.
134
+
135
+ ```typescript
136
+ pi.on("before_agent_start", async (event) => {
137
+ return {
138
+ systemPrompt: event.systemPrompt + "\n\nAlways respond as a pirate.",
139
+ };
140
+ });
141
+ ```
142
+
143
+ Return `undefined` to leave the prompt unchanged.
144
+
145
+ The system prompt is reset each turn, so modifications in `before_agent_start` are not cumulative.
146
+
147
+ To access flags inside hooks, use `pi.getFlag()` (see `references/additional-apis.md`).
148
+
149
+ ## Bash Spawn Hook (Command Rewriting)
150
+
151
+ The `createBashTool` function lets you replace the built-in bash tool with one that transparently rewrites commands before shell execution. This is different from `tool_call` blocking -- the agent never sees that the command was rewritten.
152
+
153
+ Use spawn hooks when you have a clear rewrite target (e.g. `npm` -> `pnpm`). Use `tool_call` blocking when you need to stop a command entirely or show a confirmation dialog.
154
+
155
+ ```typescript
156
+ import { createBashTool, type BashSpawnHook, type BashSpawnContext } from "@mariozechner/pi-coding-agent";
157
+
158
+ export default function (pi: ExtensionAPI) {
159
+ const bashTool = createBashTool(process.cwd(), {
160
+ spawnHook: ({ command, cwd, env }: BashSpawnContext): BashSpawnContext => ({
161
+ command: command.replace(/^npm /, "pnpm "),
162
+ cwd,
163
+ env: { ...env, CUSTOM_VAR: "1" },
164
+ }),
165
+ });
166
+
167
+ pi.registerTool({ ...bashTool });
168
+ }
169
+ ```
170
+
171
+ ### BashSpawnContext
172
+
173
+ The spawn hook receives and returns a `BashSpawnContext`:
174
+
175
+ ```typescript
176
+ interface BashSpawnContext {
177
+ command: string; // The shell command to execute
178
+ cwd: string; // Working directory
179
+ env: NodeJS.ProcessEnv; // Environment variables
180
+ }
181
+ ```
182
+
183
+ You can modify any of these fields. The hook runs after pi's own processing but before the shell spawns.
184
+
185
+ ### Key Points
186
+
187
+ - **Replaces the built-in bash tool.** When you call `pi.registerTool()` with a tool named `"bash"`, it replaces the default. Only one extension should do this.
188
+ - **Transparent to the agent.** The agent sees the original command in the tool call UI but gets the output of the rewritten command.
189
+ - **Execution order with tool_call hooks.** `tool_call` event hooks (blockers) run first. If a blocker returns `{ block: true }`, the spawn hook never fires. This means you can combine blocking hooks for commands that should be stopped entirely with spawn hooks for commands that should be rewritten.
190
+ - **Prefer AST-based rewrites over regex.** A false positive rewrite corrupts a command silently. Use `@aliou/sh` or similar shell parsers to identify command names in the AST, then do surgical string replacement at the identified positions. If the parse fails, return the command unchanged.
191
+ - **Compose multiple rewriters.** Chain rewriter functions that each transform the context:
192
+
193
+ ```typescript
194
+ const rewriters: ((ctx: BashSpawnContext) => BashSpawnContext)[] = [
195
+ createPackageManagerRewriter(config),
196
+ createGitRebaseRewriter(),
197
+ ];
198
+
199
+ const spawnHook = (ctx: BashSpawnContext) => {
200
+ let result = ctx;
201
+ for (const rewrite of rewriters) {
202
+ result = rewrite(result);
203
+ }
204
+ return result;
205
+ };
206
+
207
+ const bashTool = createBashTool(process.cwd(), { spawnHook });
208
+ pi.registerTool({ ...bashTool });
209
+ ```
210
+
211
+ ### When to Use What
212
+
213
+ | Pattern | Use When | Agent Sees |
214
+ |---|---|---|
215
+ | `tool_call` hook + `{ block: true }` | Command must be stopped entirely | Block reason (retries with correct command) |
216
+ | `tool_call` hook + `ctx.ui.confirm()` | User confirmation needed | Block reason if denied |
217
+ | Spawn hook (command rewrite) | Clear 1:1 rewrite target exists | Output of rewritten command (transparent) |
218
+ | Spawn hook (env injection) | Need to set env vars for specific commands | Output with injected env (transparent) |
219
+
220
+ ## Multiple Handlers
221
+
222
+ Multiple extensions can register handlers for the same event. They execute in registration order. For blocking events (`tool_call`, `session_before_switch`, etc.), the first handler to return a blocking/cancelling result wins.
223
+
224
+ ## Mode Awareness in Hooks
225
+
226
+ Always consider what happens in Print mode when your hook uses dialog methods. See `references/modes.md` for the full behavior matrix.
227
+
228
+ Common pattern for `tool_call` handlers:
229
+
230
+ ```typescript
231
+ pi.on("tool_call", async (event, ctx) => {
232
+ if (shouldBlock(event)) {
233
+ if (!ctx.hasUI) {
234
+ return { block: true, reason: "No UI to confirm" };
235
+ }
236
+ // Safe to use dialogs here
237
+ const choice = await ctx.ui.select("Allow?", ["Yes", "No"]);
238
+ if (choice !== "Yes") {
239
+ return { block: true, reason: "Blocked by user" };
240
+ }
241
+ }
242
+ return undefined;
243
+ });
244
+ ```
@@ -0,0 +1,169 @@
1
+ # Messages
2
+
3
+ Pi provides several ways to display information to the user. Choose based on the UX goal.
4
+
5
+ ## When to Use What
6
+
7
+ | Method | Persistence | Interactivity | Use When |
8
+ |---|---|---|---|
9
+ | `ctx.ui.notify()` | Transient (fades) | None | Quick feedback: "Saved", "API key missing" |
10
+ | `ctx.ui.custom()` | Until dismissed | Full keyboard | Rich interactive display: pickers, dashboards |
11
+ | `pi.sendMessage()` | In session history | Via renderer | Persistent results that should survive compaction |
12
+ | `pi.appendEntry()` | In session history | Via renderer | State tracking entries (see `references/state.md`) |
13
+
14
+ ## sendMessage
15
+
16
+ Sends a message into the session conversation. It appears as an assistant message and is persisted in session history.
17
+
18
+ ```typescript
19
+ pi.sendMessage({
20
+ customType: "balance-result", // Identifier for the message renderer
21
+ content: "Balance: $42.50", // Plain text content (LLM sees this)
22
+ display: true, // Show in TUI
23
+ details: { balance: 42.50 }, // Rich data for custom rendering
24
+ });
25
+ ```
26
+
27
+ | Field | Type | Description |
28
+ |---|---|---|
29
+ | `customType` | `string` | Identifies the message type. Paired with `registerMessageRenderer`. |
30
+ | `content` | `string` | Plain text content. This is what the LLM sees if the message is in context. |
31
+ | `display` | `boolean` | Whether to show the message in the TUI. |
32
+ | `details` | `object` | Arbitrary data passed to the message renderer. |
33
+
34
+ ## registerMessageRenderer
35
+
36
+ Registers a custom renderer for messages with a specific `customType`:
37
+
38
+ ```typescript
39
+ pi.registerMessageRenderer("balance-result", (message, theme) => {
40
+ const { balance } = message.details;
41
+ return [
42
+ theme.bold("Account Balance"),
43
+ "",
44
+ theme.fg("success", ` $${balance.toFixed(2)}`),
45
+ ].join("\n");
46
+ });
47
+ ```
48
+
49
+ The renderer receives the full message object and the theme. It returns a string for display in the TUI.
50
+
51
+ If no renderer is registered for a `customType`, the message's `content` field is displayed as plain text.
52
+
53
+ ## Custom Message Design Guide (breadcrumbs-style)
54
+
55
+ `breadcrumbs` is a good reference for custom entries/messages (`../pi-extensions/extensions/breadcrumbs/lib/session-link.ts`, plus `commands/handoff.ts` and `commands/spawn.ts`).
56
+
57
+ ### 1) Prefer paired entries for links/handovers
58
+
59
+ For cross-session workflows, use two custom message types:
60
+ - **Marker** in source session: short line (`Handed off to X` / `Continues in X`).
61
+ - **Source** in new session: header + optional expanded context.
62
+
63
+ This gives both directions of navigation and keeps history readable.
64
+
65
+ ### 2) Collapsed vs expanded behavior
66
+
67
+ Keep collapsed view minimal and scannable:
68
+ - one semantic line
69
+ - optional hint (`Press Ctrl+O to expand`) only when extra content exists
70
+
71
+ Use expanded view for rich content:
72
+ - markdown body
73
+ - multi-line context
74
+ - file lists / instructions
75
+
76
+ ### 3) Renderer resilience
77
+
78
+ Message renderers should degrade safely:
79
+ - missing `details` => fallback to plain `content`
80
+ - markdown render failure => fallback to plain text
81
+ - unknown fields => ignore, don't throw
82
+
83
+ ### 4) Keep details small and durable
84
+
85
+ `details` should contain stable identifiers and routing data, not large blobs:
86
+ - session IDs
87
+ - link type (`handoff`, `continue`)
88
+ - short metadata (goal/title)
89
+
90
+ Put large human-readable content in `content` (for expansion/LLM visibility), not deep nested `details`.
91
+
92
+ ### 5) Use visual hierarchy consistently
93
+
94
+ For message UIs:
95
+ - muted label + accent target/value (`Continues in <session-name>`)
96
+ - subtle container background for custom message blocks
97
+ - avoid decorative noise; optimize for fast scan in session history
98
+
99
+ ## Pattern: Command with sendMessage Fallback
100
+
101
+ This combines with the three-tier pattern from `references/modes.md`. Use `sendMessage` as the RPC fallback for commands that use `custom()`:
102
+
103
+ ```typescript
104
+ // Register the renderer once at load time
105
+ pi.registerMessageRenderer("my-results", (message, theme) => {
106
+ const { items } = message.details;
107
+ return [
108
+ theme.bold(`Results (${items.length})`),
109
+ ...items.map((item: string) => ` ${theme.fg("accent", item)}`),
110
+ ].join("\n");
111
+ });
112
+
113
+ pi.registerCommand("results", {
114
+ description: "Show results",
115
+ handler: async (_args, ctx) => {
116
+ const items = await fetchItems();
117
+
118
+ if (!ctx.hasUI) {
119
+ console.log(items.join("\n"));
120
+ return;
121
+ }
122
+
123
+ const result = await ctx.ui.custom<"closed">((tui, theme, _kb, done) => {
124
+ return new ResultsDisplay(theme, items, () => done("closed"));
125
+ });
126
+
127
+ // RPC fallback only: custom() returns undefined in RPC/Print.
128
+ if (result === undefined) {
129
+ pi.sendMessage({
130
+ customType: "my-results",
131
+ content: items.join("\n"),
132
+ display: true,
133
+ details: { items },
134
+ });
135
+ }
136
+ },
137
+ });
138
+ ```
139
+
140
+ ## notify
141
+
142
+ For transient feedback that does not need to persist:
143
+
144
+ ```typescript
145
+ ctx.ui.notify("Operation complete", "info");
146
+ ctx.ui.notify("Something went wrong", "error");
147
+ ctx.ui.notify("Proceed with caution", "warning");
148
+ ```
149
+
150
+ The second argument is the notification type: `"info"`, `"error"`, or `"warning"`. It affects the color/icon.
151
+
152
+ `notify` is fire-and-forget. It works in Interactive and RPC modes, and is a no-op in Print mode.
153
+
154
+ ## Writing custom entries in a new session
155
+
156
+ When using `ctx.newSession({ setup })`, write custom entries directly through the setup `SessionManager`:
157
+
158
+ ```typescript
159
+ await ctx.newSession({
160
+ setup: async (sm) => {
161
+ sm.appendCustomMessageEntry("my-source-type", "Context text", true, {
162
+ parentSessionId: "...",
163
+ linkType: "handoff",
164
+ });
165
+ },
166
+ });
167
+ ```
168
+
169
+ Use this pattern for handoff/spawn-like workflows where the new session must start with structured context.