indusagi 0.12.33 → 0.13.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/agent.js +1247 -184
  3. package/dist/ai.js +72 -4
  4. package/dist/capabilities.js +69 -2
  5. package/dist/cli.js +1353 -29
  6. package/dist/connectors-saas.js +66 -0
  7. package/dist/index.js +1353 -29
  8. package/dist/interop.js +66 -0
  9. package/dist/mcp.js +270 -363
  10. package/dist/react-ink.js +1391 -41
  11. package/dist/shell-app.js +1353 -29
  12. package/dist/smithy.js +69 -2
  13. package/dist/swarm.js +69 -2
  14. package/dist/types/capabilities/backends/node-backends.d.ts +3 -1
  15. package/dist/types/capabilities/files/read-state-gate.d.ts +69 -0
  16. package/dist/types/capabilities/files/read-state-gate.test.d.ts +14 -0
  17. package/dist/types/capabilities/kernel/context.d.ts +4 -0
  18. package/dist/types/capabilities/kernel/index.d.ts +2 -2
  19. package/dist/types/capabilities/kernel/spec.d.ts +55 -0
  20. package/dist/types/facade/bot/actions/bash.d.ts +15 -0
  21. package/dist/types/facade/bot/actions/bash.test.d.ts +1 -0
  22. package/dist/types/facade/bot/actions/checkpoint.d.ts +49 -0
  23. package/dist/types/facade/bot/actions/checkpoint.test.d.ts +1 -0
  24. package/dist/types/facade/bot/actions/edit-utils.d.ts +86 -0
  25. package/dist/types/facade/bot/actions/edit.d.ts +18 -0
  26. package/dist/types/facade/bot/actions/edit.test.d.ts +1 -0
  27. package/dist/types/facade/bot/actions/find.d.ts +2 -0
  28. package/dist/types/facade/bot/actions/find.test.d.ts +1 -0
  29. package/dist/types/facade/bot/actions/grep.d.ts +10 -0
  30. package/dist/types/facade/bot/actions/grep.test.d.ts +1 -0
  31. package/dist/types/facade/bot/actions/index.d.ts +16 -0
  32. package/dist/types/facade/bot/actions/read-state.d.ts +83 -0
  33. package/dist/types/facade/bot/actions/read-state.test.d.ts +1 -0
  34. package/dist/types/facade/bot/actions/read.d.ts +7 -0
  35. package/dist/types/facade/bot/actions/read.test.d.ts +1 -0
  36. package/dist/types/facade/bot/actions/sandbox-backend.d.ts +99 -0
  37. package/dist/types/facade/bot/actions/sandbox-backend.test.d.ts +1 -0
  38. package/dist/types/facade/bot/actions/websearch.d.ts +5 -2
  39. package/dist/types/facade/bot/actions/websearch.test.d.ts +1 -0
  40. package/dist/types/facade/bot/actions/write.d.ts +15 -0
  41. package/dist/types/facade/bot/agent-loop.d.ts +10 -0
  42. package/dist/types/facade/bot/agent-loop.test.d.ts +1 -0
  43. package/dist/types/facade/bot/agent.d.ts +9 -1
  44. package/dist/types/facade/bot/permission-gate.test.d.ts +1 -0
  45. package/dist/types/facade/bot/types.d.ts +60 -0
  46. package/dist/types/facade/mcp-core/client.d.ts +71 -15
  47. package/dist/types/facade/mcp-core/client.test.d.ts +18 -0
  48. package/dist/types/facade/mcp-core/types.d.ts +10 -0
  49. package/dist/types/facade/ml/adapters/anthropic-retry.test.d.ts +1 -0
  50. package/dist/types/facade/ml/adapters/anthropic.d.ts +17 -0
  51. package/dist/types/facade/ml/adapters/simple-options.d.ts +13 -0
  52. package/dist/types/facade/ml/adapters/simple-options.test.d.ts +1 -0
  53. package/dist/types/react-ink/components/StatusLine.d.ts +10 -1
  54. package/dist/types/react-ink/components/ToolEventBlock.d.ts +9 -1
  55. package/dist/types/react-ink/components/ToolEventBlock.test.d.ts +1 -0
  56. package/dist/types/react-ink/components/dialogs/SelectableDialog.d.ts +7 -1
  57. package/dist/types/react-ink/components/dialogs/ThemeDialog.d.ts +21 -2
  58. package/dist/types/react-ink/diff/Diff.d.ts +22 -0
  59. package/dist/types/react-ink/diff/diff.test.d.ts +1 -0
  60. package/dist/types/react-ink/diff/structured.d.ts +41 -0
  61. package/dist/types/react-ink/diff/word-diff.d.ts +27 -0
  62. package/dist/types/react-ink/index.d.ts +8 -0
  63. package/dist/types/react-ink/markdown/Markdown.d.ts +23 -0
  64. package/dist/types/react-ink/markdown/MarkdownTable.d.ts +19 -0
  65. package/dist/types/react-ink/markdown/StreamingMarkdown.d.ts +34 -0
  66. package/dist/types/react-ink/markdown/format-token.d.ts +39 -0
  67. package/dist/types/react-ink/markdown/highlight.d.ts +31 -0
  68. package/dist/types/react-ink/theme-adapter.d.ts +58 -1
  69. package/dist/types/react-ink/utils/tool-display.d.ts +17 -1
  70. package/package.json +5 -1
@@ -4,7 +4,18 @@
4
4
  * Manages connection to one MCP server and provides
5
5
  * methods to list/call tools, read resources, etc.
6
6
  *
7
- * Reference: MCP client implementation patterns.
7
+ * Transport is provided by the official `@modelcontextprotocol/sdk`:
8
+ * - stdio → subprocess JSON-RPC over stdin/stdout
9
+ * - http → Streamable HTTP (POST for client→server, plus a server→client
10
+ * push channel — the SDK opens the SSE stream and a standalone GET
11
+ * SSE for server-initiated messages, and handles reconnect/
12
+ * session-expiry by re-using the session id)
13
+ * - sse → legacy HTTP+SSE transport (when `transport: "sse"` is requested)
14
+ *
15
+ * The SDK gives us a real persistent connection, a server→client push channel,
16
+ * dispatch of server NOTIFICATIONS, answering of server REQUESTS (elicitation,
17
+ * roots/list, sampling, ping), and bounded reconnect on a dropped stream /
18
+ * expired session — none of which the hand-rolled POST-only transport did.
8
19
  */
9
20
  import type { MCPServerConfig, MCPToolDefinition, MCPResource, MCPPrompt, MCPToolCallResult, MCPLogHandler, MCPProgressHandler, MCPElicitationHandler, MCPRoot } from "./types.js";
10
21
  /**
@@ -29,7 +40,7 @@ export interface MCPClientOptions {
29
40
  /**
30
41
  * MCP Client - manages connection to a single MCP server.
31
42
  *
32
- * Supports stdio transport (subprocess communication).
43
+ * Supports stdio (subprocess) and HTTP (Streamable HTTP / legacy SSE) transports.
33
44
  * Provides methods to list tools, call tools, read resources, etc.
34
45
  *
35
46
  * @example
@@ -50,10 +61,8 @@ export interface MCPClientOptions {
50
61
  */
51
62
  export declare class MCPClient {
52
63
  private options;
53
- private process?;
54
- private buffer;
55
- private messageId;
56
- private pendingRequests;
64
+ private client?;
65
+ private transport?;
57
66
  private isConnected;
58
67
  private connectionPromise;
59
68
  private serverCapabilities?;
@@ -61,6 +70,13 @@ export declare class MCPClient {
61
70
  private enableServerLogs;
62
71
  private enableProgressTracking;
63
72
  private _roots;
73
+ private elicitationHandler?;
74
+ private samplingHandler?;
75
+ private resourceUpdatedHandler?;
76
+ private resourceListChangedHandler?;
77
+ private toolListChangedHandler?;
78
+ private promptListChangedHandler?;
79
+ private progressHandler?;
64
80
  /** Server name */
65
81
  readonly serverName: string;
66
82
  /** Server config */
@@ -74,10 +90,37 @@ export declare class MCPClient {
74
90
  */
75
91
  connect(): Promise<void>;
76
92
  private doConnect;
77
- private connectHttp;
78
- private connectStdio;
93
+ /**
94
+ * Build the SDK transport for the configured server.
95
+ *
96
+ * - stdio: `StdioClientTransport` (inherits the parent env, merges config.env).
97
+ * - http: `StreamableHTTPClientTransport` (POST + server-push SSE channel,
98
+ * reconnect + session-expiry handled by the SDK), unless the config
99
+ * asks for the legacy `SSEClientTransport`.
100
+ */
101
+ private createTransport;
102
+ /**
103
+ * Register handlers for server-initiated REQUESTS and NOTIFICATIONS.
104
+ *
105
+ * REQUESTS (have both `.method` AND `.id`) must be ANSWERED with a JSON-RPC
106
+ * RESPONSE carrying the matching id — the SDK does this automatically for the
107
+ * value a request handler returns (or rejects). The previous hand-rolled
108
+ * transport routed these into the notification path, so they were never
109
+ * answered (bug #13).
110
+ *
111
+ * Defaults:
112
+ * - ping → {}
113
+ * - roots/list → the configured roots
114
+ * - elicitation/create→ decline ({action:"cancel"}) unless a host handler is set
115
+ * - sampling → reject "Method not found" unless a host handler is set
116
+ */
117
+ private registerServerHandlers;
79
118
  /**
80
119
  * Disconnect from the MCP server.
120
+ *
121
+ * The SDK's `client.close()` closes the transport and rejects any pending
122
+ * requests — a clean teardown for both stdio (graceful subprocess shutdown)
123
+ * and HTTP (close the push channel / end the session).
81
124
  */
82
125
  disconnect(): Promise<void>;
83
126
  /**
@@ -134,24 +177,37 @@ export declare class MCPClient {
134
177
  * Set a handler for resource list changed notifications.
135
178
  */
136
179
  setResourceListChangedHandler(handler: () => void): void;
180
+ /**
181
+ * Set a handler for tool list changed notifications.
182
+ */
183
+ setToolListChangedHandler(handler: () => void): void;
137
184
  /**
138
185
  * Set a handler for prompt list changed notifications.
139
186
  */
140
187
  setPromptListChangedHandler(handler: () => void): void;
141
188
  /**
142
- * Set a handler for elicitation requests.
189
+ * Set a handler for elicitation requests. When set, the server's
190
+ * `elicitation/create` REQUEST is answered with the host's decision; when
191
+ * unset the request is declined ({action:"cancel"}).
143
192
  */
144
193
  setElicitationHandler(handler: MCPElicitationHandler): void;
194
+ /**
195
+ * Set a handler for sampling (`sampling/createMessage`) requests. When set,
196
+ * the server request is answered with the host's result; when unset the
197
+ * request is rejected with "Method not found".
198
+ */
199
+ setSamplingHandler(handler: (params: unknown) => Promise<unknown>): void;
145
200
  /**
146
201
  * Set a handler for progress notifications.
147
202
  */
148
203
  setProgressHandler(handler: MCPProgressHandler): void;
149
204
  private ensureConnected;
150
- private sendRequest;
151
- private sendHttpRequest;
152
- private sendNotification;
153
- private processBuffer;
154
- private handleMessage;
155
- private handleNotification;
205
+ /**
206
+ * Normalize an SDK/transport error into an MCPError. Session-expiry style
207
+ * failures (404 session / -32001) are surfaced as SESSION_ERROR so callers
208
+ * (the pool) can decide to reconnect; the SDK's StreamableHTTP transport
209
+ * already attempts a bounded reconnect re-using the session id before this.
210
+ */
211
+ private wrapError;
156
212
  private log;
157
213
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * MCP client transport tests (#23 / bug #13).
3
+ *
4
+ * These verify the server→client directions that the old hand-rolled,
5
+ * POST-only transport never handled:
6
+ * 1. A server-initiated REQUEST (`elicitation/create`) is ANSWERED with a
7
+ * JSON-RPC RESPONSE carrying the matching id (not silently dropped into a
8
+ * notification path).
9
+ * 2. A server-initiated NOTIFICATION (`notifications/tools/list_changed`)
10
+ * fires the host-registered handler.
11
+ * 3. The HTTP path builds a real Streamable-HTTP transport with a server-push
12
+ * channel, and an SSE-framed server message off that channel is parsed and
13
+ * dispatched to the right handler.
14
+ *
15
+ * A `MockTransport` (implementing the SDK `Transport` interface) drives the
16
+ * client without a live MCP server.
17
+ */
18
+ export {};
@@ -27,6 +27,12 @@ export interface HttpServerConfig {
27
27
  url: URL;
28
28
  /** Optional headers for HTTP requests */
29
29
  headers?: Record<string, string>;
30
+ /**
31
+ * Which HTTP transport to use.
32
+ * - "http" (default): Streamable HTTP (single endpoint, server-push channel)
33
+ * - "sse": legacy HTTP+SSE transport
34
+ */
35
+ transport?: "http" | "sse";
30
36
  /** Custom fetch implementation */
31
37
  fetch?: typeof fetch;
32
38
  }
@@ -166,6 +172,8 @@ export interface MCPServerConfigEntry {
166
172
  url?: string;
167
173
  /** Headers for HTTP transport */
168
174
  headers?: Record<string, string>;
175
+ /** HTTP transport flavor ("http" = Streamable HTTP default, "sse" = legacy) */
176
+ transport?: "http" | "sse";
169
177
  /** Timeout in milliseconds */
170
178
  timeout?: number;
171
179
  /** Whether this server is enabled (default: true) */
@@ -185,6 +193,8 @@ export interface MCPServerConfigValue {
185
193
  url?: string;
186
194
  /** Headers for HTTP transport */
187
195
  headers?: Record<string, string>;
196
+ /** HTTP transport flavor ("http" = Streamable HTTP default, "sse" = legacy) */
197
+ transport?: "http" | "sse";
188
198
  /** Timeout in milliseconds */
189
199
  timeout?: number;
190
200
  /** Whether this server is enabled (default: true) */
@@ -57,5 +57,22 @@ export declare class AnthropicStreamHandler extends BaseStreamHandler {
57
57
  */
58
58
  process(anthropicStream: AsyncIterable<unknown>, onEvent: (event: any) => void): Promise<void>;
59
59
  }
60
+ /**
61
+ * Reads a `Retry-After` header off an Anthropic APIError and converts it to a
62
+ * delay in milliseconds. The header is the integer-seconds form per the
63
+ * Anthropic API; a missing/non-numeric value yields `null` so callers fall
64
+ * back to exponential backoff.
65
+ */
66
+ export declare function parseAnthropicRetryAfterMs(error: unknown): number | null;
67
+ /**
68
+ * Classifies an Anthropic failure as worth retrying. Mirrors
69
+ * `kit/provider-errors.ts:isRetryableError`: transient HTTP statuses (408/409/
70
+ * 429/529/5xx) and an `overloaded_error` body are retryable; auth/validation
71
+ * (401/403/4xx) are not. Generic errors fall back to a message sniff covering
72
+ * network/timeout/overload conditions. The `_attempt` is accepted to satisfy
73
+ * the RetryPolicy.shouldRetry contract but unused here (the loop already caps
74
+ * attempts).
75
+ */
76
+ export declare function shouldRetryAnthropic(error: unknown, _attempt: number): boolean;
60
77
  export declare const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOptions>;
61
78
  export declare const streamSimpleAnthropic: StreamFunction<"anthropic-messages", SimpleStreamOptions>;
@@ -8,7 +8,20 @@ export interface RetryPolicy {
8
8
  maxAttempts: number;
9
9
  baseDelayMs: number;
10
10
  maxDelayMs?: number;
11
+ /**
12
+ * When present, abort is checked between attempts and before each backoff
13
+ * sleep. A live request is short-circuited the moment its signal fires —
14
+ * but the presence of a signal no longer suppresses transient retries.
15
+ */
16
+ signal?: AbortSignal;
11
17
  shouldRetry?: (error: unknown, attempt: number) => boolean;
18
+ /**
19
+ * Returns the server-requested cooldown (e.g. a parsed `Retry-After`
20
+ * header) in milliseconds, or `null` when the error carries no such hint.
21
+ * When provided, it takes priority over exponential backoff (still clamped
22
+ * by `maxDelayMs`).
23
+ */
24
+ getRetryAfterMs?: (error: unknown) => number | null;
12
25
  }
13
26
  export declare class SimpleOptionsProviderError extends Error {
14
27
  readonly code: "rate_limit" | "timeout" | "network" | "validation" | "auth" | "unknown";
@@ -4,6 +4,15 @@ interface StatusLineProps {
4
4
  theme: InkThemeAdapter;
5
5
  status?: StatusMessage;
6
6
  snapshot: SessionSnapshot;
7
+ /**
8
+ * Whether this line draws its own "Agent working..." / "Running bash..." /
9
+ * "Compacting..." busy fallbacks. Defaults to `true` (legacy behaviour). A host
10
+ * that renders its own live progress affordance (e.g. an animated working
11
+ * indicator on a separate row) passes `false` so the busy state is shown in ONE
12
+ * place only — this line then carries just the explicit transient toast (and a
13
+ * hard error), and never competes with the host's indicator.
14
+ */
15
+ showBusyText?: boolean;
7
16
  }
8
- export declare function StatusLine({ snapshot, status, theme }: StatusLineProps): JSX.Element | null;
17
+ export declare function StatusLine({ snapshot, status, theme, showBusyText }: StatusLineProps): JSX.Element | null;
9
18
  export {};
@@ -12,6 +12,14 @@ interface ToolEventBlockProps {
12
12
  showTitle?: boolean;
13
13
  showSummaryInline?: boolean;
14
14
  maxContentLines?: number;
15
+ /**
16
+ * When true the detail body already carries syntax-highlight ANSI; render it
17
+ * verbatim instead of stripping the escapes. stripAnsi is still used for the
18
+ * visible-width / line-count measurements so wrapping and clamping stay
19
+ * correct against the escape-free text.
20
+ */
21
+ preformatted?: boolean;
15
22
  }
16
- export declare function ToolEventBlock({ detail, emptyText, indent, marginBottom, maxContentLines, showSummaryInline, showTitle, status, summary, theme: _theme, title, }: ToolEventBlockProps): JSX.Element;
23
+ export declare function statusColorKey(status: ToolEventStatus): string;
24
+ export declare function ToolEventBlock({ detail, emptyText, indent, marginBottom, maxContentLines, preformatted, showSummaryInline, showTitle, status, summary, theme, title, }: ToolEventBlockProps): JSX.Element;
17
25
  export {};
@@ -9,8 +9,14 @@ interface SelectableDialogProps<T> {
9
9
  noSearchResultsText?: string;
10
10
  onClose: () => void;
11
11
  onSelect: (item: T) => void | Promise<void>;
12
+ /**
13
+ * Fired whenever the highlighted item changes (including on first mount), so a
14
+ * caller can live-preview the currently-focused choice before it is committed
15
+ * with Enter. Distinct from {@link onSelect}, which fires only on Enter.
16
+ */
17
+ onHighlight?: (item: T, index: number) => void;
12
18
  getSearchText?: (item: T) => string;
13
19
  renderItem: (item: T, selected: boolean, index: number) => ReactNode;
14
20
  }
15
- export declare function SelectableDialog<T>({ title, subtitle, items, isActive, searchEnabled, emptyText, noSearchResultsText, onClose, onSelect, getSearchText, renderItem, }: SelectableDialogProps<T>): JSX.Element;
21
+ export declare function SelectableDialog<T>({ title, subtitle, items, isActive, searchEnabled, emptyText, noSearchResultsText, onClose, onSelect, onHighlight, getSearchText, renderItem, }: SelectableDialogProps<T>): JSX.Element;
16
22
  export {};
@@ -1,7 +1,26 @@
1
+ /**
2
+ * A single selectable scheme row. The `id` is the scheme token committed/
3
+ * previewed; `label` and `description` are the human-readable strings shown.
4
+ */
5
+ export interface ThemeDialogItem {
6
+ id: string;
7
+ label: string;
8
+ description?: string;
9
+ }
1
10
  interface ThemeDialogProps {
2
- themes: string[];
11
+ /**
12
+ * The schemes to offer. Either bare scheme tokens (label == token) or rich
13
+ * items carrying a friendly label + description.
14
+ */
15
+ themes: Array<string | ThemeDialogItem>;
3
16
  onClose: () => void;
17
+ /** Fired on Enter: the chosen scheme token. */
4
18
  onSelect: (themeName: string) => void | Promise<void>;
19
+ /**
20
+ * Fired on every highlight move (and on mount): the focused scheme token, so
21
+ * the caller can live-preview a scheme before it is committed.
22
+ */
23
+ onHighlight?: (themeName: string) => void;
5
24
  }
6
- export declare function ThemeDialog({ themes, onClose, onSelect }: ThemeDialogProps): JSX.Element;
25
+ export declare function ThemeDialog({ themes, onClose, onSelect, onHighlight }: ThemeDialogProps): JSX.Element;
7
26
  export {};
@@ -0,0 +1,22 @@
1
+ import type { ReactNode } from "indusagi/react-host";
2
+ import type { InkThemeAdapter } from "../theme-adapter.js";
3
+ import type { StructuredDiff } from "./structured.js";
4
+ interface DiffProps {
5
+ diff: StructuredDiff;
6
+ theme: InkThemeAdapter;
7
+ /** Left margin (columns) applied to the whole block. */
8
+ indent?: number;
9
+ marginBottom?: number;
10
+ }
11
+ /**
12
+ * A minimal, pure-Ink colored diff block.
13
+ *
14
+ * Renders each hunk's classified lines with a right-aligned line-number gutter,
15
+ * themed add/remove backgrounds, and word-level intra-line emphasis (only the
16
+ * changed sub-ranges are bolded; suppressed above the change threshold upstream
17
+ * in {@link buildStructuredDiff}). Multiple hunks are separated by a dim `...`
18
+ * marker. No state, no effects — colors survive to the terminal because nothing
19
+ * strips the ANSI on this path.
20
+ */
21
+ export declare function Diff({ diff, theme, indent, marginBottom }: DiffProps): ReactNode;
22
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,41 @@
1
+ import { type WordSpan } from "./word-diff.js";
2
+ /** Lines of unchanged context kept around each change, matching git's default. */
3
+ export declare const CONTEXT_LINES = 3;
4
+ /** A single rendered diff line, already classified and numbered. */
5
+ export interface DiffLine {
6
+ kind: "context" | "added" | "removed";
7
+ /** The old-file line number, or undefined for added lines. */
8
+ oldLine?: number;
9
+ /** The new-file line number, or undefined for removed lines. */
10
+ newLine?: number;
11
+ /** The line text without its `+`/`-`/space diff prefix. */
12
+ text: string;
13
+ /**
14
+ * Word-level segments for intra-line highlighting. Present only on added /
15
+ * removed lines that were paired with their counterpart; context lines and
16
+ * unpaired changes leave this undefined (render the whole line one colour).
17
+ */
18
+ spans?: WordSpan[];
19
+ }
20
+ /** One contiguous change region: classified lines plus a hunk header marker. */
21
+ export interface DiffHunk {
22
+ oldStart: number;
23
+ newStart: number;
24
+ lines: DiffLine[];
25
+ }
26
+ export interface StructuredDiff {
27
+ hunks: DiffHunk[];
28
+ addedCount: number;
29
+ removedCount: number;
30
+ }
31
+ /**
32
+ * Build a structured diff between `oldStr` and `newStr` using the `diff`
33
+ * package's {@link structuredPatch} with {@link CONTEXT_LINES} of context.
34
+ *
35
+ * The raw hunk `lines` (prefixed `+`/`-`/space) are walked into typed
36
+ * {@link DiffLine}s carrying gutter line numbers. Adjacent removed→added runs
37
+ * are paired index-for-index and handed to {@link wordDiffLine} so the renderer
38
+ * can emphasise only the changed sub-range of each line. Returns `null` when
39
+ * there is nothing to show (identical input or an empty patch).
40
+ */
41
+ export declare function buildStructuredDiff(oldStr: string, newStr: string, filePath?: string): StructuredDiff | null;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Above this fraction of a line changing, intra-line word highlighting reads as
3
+ * noise — almost every span is "changed", so a flat whole-line colour is
4
+ * clearer. Below it we paint only the genuinely altered sub-ranges brighter.
5
+ */
6
+ export declare const CHANGE_THRESHOLD = 0.4;
7
+ /** One contiguous run of a removed/added line, flagged whether it changed. */
8
+ export interface WordSpan {
9
+ text: string;
10
+ /** True when this run differs between the old and new line. */
11
+ changed: boolean;
12
+ }
13
+ /**
14
+ * Pair a removed line with the added line that replaced it and return the
15
+ * per-segment word diff for ONE side.
16
+ *
17
+ * `side` selects which line's segments we want: a removed-line render wants the
18
+ * segments present in the old text (unchanged + removed), an added-line render
19
+ * wants the segments present in the new text (unchanged + added). The other
20
+ * side's segments are dropped so the caller paints exactly the line it is on.
21
+ *
22
+ * Word highlighting is suppressed — the whole line is returned as a single
23
+ * `changed: true` span — when the changed character fraction exceeds
24
+ * {@link CHANGE_THRESHOLD}, where scattered word spans read worse than a flat
25
+ * line colour. The returned spans always concatenate back to the original line.
26
+ */
27
+ export declare function wordDiffLine(oldLine: string, newLine: string, side: "removed" | "added"): WordSpan[];
@@ -29,6 +29,14 @@ export * from "./components/messages/SkillInvocationMessage.js";
29
29
  export * from "./components/messages/ToolCallMessage.js";
30
30
  export * from "./components/messages/ToolResultBlock.js";
31
31
  export * from "./components/messages/UserMessage.js";
32
+ export * from "./diff/Diff.js";
33
+ export * from "./diff/structured.js";
34
+ export * from "./diff/word-diff.js";
35
+ export * from "./markdown/Markdown.js";
36
+ export * from "./markdown/MarkdownTable.js";
37
+ export * from "./markdown/StreamingMarkdown.js";
38
+ export * from "./markdown/format-token.js";
39
+ export * from "./markdown/highlight.js";
32
40
  export * from "./utils/message-groups.js";
33
41
  export * from "./utils/selection-dialog.js";
34
42
  export * from "./utils/tool-display.js";
@@ -0,0 +1,23 @@
1
+ import type { ReactNode } from "indusagi/react-host";
2
+ import type { InkThemeAdapter } from "../theme-adapter.js";
3
+ interface MarkdownProps {
4
+ /** The raw markdown source to render. */
5
+ children: string;
6
+ /** The theme adapter colours resolve against. */
7
+ theme: InkThemeAdapter;
8
+ /** When false, fenced code is rendered plain (no syntax highlighting). */
9
+ highlightCode?: boolean;
10
+ /** Render every text run dim (used for de-emphasised content). */
11
+ dim?: boolean;
12
+ }
13
+ /**
14
+ * Render markdown as styled terminal output.
15
+ *
16
+ * Tables become real flexbox columns ({@link MarkdownTable}); every other token
17
+ * is formatted to a chalk-ANSI string via {@link formatToken} and accumulated
18
+ * into a single `<Text>` run. Ink passes embedded ANSI straight to the terminal,
19
+ * so the chalk escapes render as colour. The `theme` is passed explicitly — the
20
+ * rebuild has no `useTheme()` hook.
21
+ */
22
+ export declare function Markdown({ children, theme, highlightCode, dim }: MarkdownProps): ReactNode;
23
+ export {};
@@ -0,0 +1,19 @@
1
+ import type { Tokens } from "marked";
2
+ import type { InkThemeAdapter } from "../theme-adapter.js";
3
+ import type { CliHighlight } from "./highlight.js";
4
+ interface MarkdownTableProps {
5
+ token: Tokens.Table;
6
+ theme: InkThemeAdapter;
7
+ highlight?: CliHighlight | null;
8
+ }
9
+ /**
10
+ * Render a GFM table token as aligned flexbox columns.
11
+ *
12
+ * Each column's width is measured from the stripAnsi'd display text (so embedded
13
+ * ANSI styling does not inflate the column), the header row is bold, a dash rule
14
+ * separates header from body, and every cell is padded per its column alignment.
15
+ * Cells render as `<Box>` columns inside `<Box flexDirection="row">` rows, so Ink
16
+ * lays them out as real columns rather than a raw `| a | b |` pipe string.
17
+ */
18
+ export declare function MarkdownTable({ token, theme, highlight }: MarkdownTableProps): JSX.Element;
19
+ export {};
@@ -0,0 +1,34 @@
1
+ import type { ReactNode } from "indusagi/react-host";
2
+ import type { InkThemeAdapter } from "../theme-adapter.js";
3
+ interface StreamingMarkdownProps {
4
+ /** The accumulated answer text so far (grows monotonically per delta). */
5
+ children: string;
6
+ /** The theme adapter colours resolve against. */
7
+ theme: InkThemeAdapter;
8
+ /** When false, fenced code is rendered plain (no syntax highlighting). */
9
+ highlightCode?: boolean;
10
+ /** Render every text run dim. */
11
+ dim?: boolean;
12
+ }
13
+ /**
14
+ * Streaming-aware markdown renderer with a monotonic stable-prefix boundary.
15
+ *
16
+ * On every delta the accumulated answer grows. Re-lexing the whole answer each
17
+ * time would re-parse (and reflow/flicker) content that is already settled. To
18
+ * avoid that, we split the answer at the last completed block boundary:
19
+ *
20
+ * - **stable prefix** — every block above the boundary. Memoised on its own
21
+ * content, so once a block settles its `<Markdown>` is parsed once and never
22
+ * re-renders as later deltas arrive.
23
+ * - **unstable suffix** — the still-growing last block. Re-lexed every delta
24
+ * (cheap: it is one block, and `Markdown`'s own LRU cache + fast-path absorb
25
+ * the cost).
26
+ *
27
+ * The boundary only ever moves forward (it is recomputed from `content`, which
28
+ * grows monotonically, and `lastStableBoundary` is monotonic in its input), so
29
+ * completed blocks never re-flicker. When the turn ends and the finalized
30
+ * assistant message takes over rendering, the same `<Markdown>` pipeline draws
31
+ * identical output, so the hand-off is seamless.
32
+ */
33
+ export declare function StreamingMarkdown({ children, theme, highlightCode, dim, }: StreamingMarkdownProps): ReactNode;
34
+ export {};
@@ -0,0 +1,39 @@
1
+ import { type Token } from "marked";
2
+ import type { InkThemeAdapter } from "../theme-adapter.js";
3
+ import type { CliHighlight } from "./highlight.js";
4
+ /**
5
+ * Configure the shared `marked` instance exactly once. The only customisation is
6
+ * disabling strikethrough: models frequently write `~` for "approximate"
7
+ * (e.g. `~100`) and almost never intend an actual strike, so we keep `~` literal.
8
+ */
9
+ export declare function configureMarked(): void;
10
+ /**
11
+ * Fast structural check for whether `text` contains any markdown syntax. Samples
12
+ * the first 500 chars — markdown markers (headings, fences, list bullets) cluster
13
+ * at the start, while long plain tails (tool output) stay markdown-free.
14
+ */
15
+ export declare function hasMarkdownSyntax(text: string): boolean;
16
+ /**
17
+ * Lex `content` into marked tokens, hitting the LRU cache and the plain-text
18
+ * fast-path. Plain text (no markdown syntax) returns a single synthetic
19
+ * paragraph token and is intentionally not cached — reconstruction is one
20
+ * allocation and caching would retain the content for no benefit.
21
+ */
22
+ export declare function cachedLexer(content: string): Token[];
23
+ /**
24
+ * Render a single marked token to a chalk-ANSI string, painting via the
25
+ * framework {@link InkThemeAdapter}. Tables are handled out-of-band by the
26
+ * `Markdown` component (rendered as flexbox), so this returns an empty string
27
+ * for `table` tokens — the caller never funnels a table through here.
28
+ */
29
+ export declare function formatToken(token: Token, theme: InkThemeAdapter, highlight?: CliHighlight | null, listDepth?: number, orderedListNumber?: number | null, parent?: Token | null): string;
30
+ /** Pick the list-marker style for a depth: 1./2. → a./b. → i./ii. → numeric. */
31
+ export declare function getListNumber(listDepth: number, orderedListNumber: number): string;
32
+ /**
33
+ * Pad `content` to `targetWidth` per alignment. `displayWidth` is the visible
34
+ * width of `content` (caller computes it on stripAnsi'd text so embedded ANSI
35
+ * codes do not skew the padding).
36
+ */
37
+ export declare function padAligned(content: string, displayWidth: number, targetWidth: number, align: "left" | "center" | "right" | null | undefined): string;
38
+ /** Measure a string's visible terminal width (ANSI-aware), for table layout. */
39
+ export declare function stringWidth(text: string): number;
@@ -0,0 +1,31 @@
1
+ import type { InkThemeAdapter } from "../theme-adapter.js";
2
+ /**
3
+ * The shape {@link formatToken} (and the tool-body highlighter) consume. It is a
4
+ * deliberately small surface: ask whether a language is known, then hand back a
5
+ * chalk-ANSI rendering of the code. Keeping it behind an interface means the
6
+ * markdown formatter never imports highlight.js directly and a caller can pass
7
+ * `null` to disable highlighting entirely.
8
+ */
9
+ export interface CliHighlight {
10
+ /** Whether highlight.js has a grammar registered for `language`. */
11
+ supportsLanguage: (language: string) => boolean;
12
+ /** Render `code` as a chalk-ANSI string, colouring scopes via the theme. */
13
+ highlight: (code: string, options: {
14
+ language: string;
15
+ }) => string;
16
+ }
17
+ /**
18
+ * Build a {@link CliHighlight} bound to a theme. The returned object paints each
19
+ * highlight.js scope via the theme's syntax roles ({@link ThemeRole}); unknown
20
+ * languages and any internal failure fall back to the raw code, never throwing.
21
+ */
22
+ export declare function createHighlighter(theme: InkThemeAdapter): CliHighlight;
23
+ /**
24
+ * Highlight `code` keyed on a file path's extension. Used by the read/write/edit
25
+ * tool bodies, which know the file but not a fenced ```lang tag. Resolves the
26
+ * extension to a highlight.js language, falling back to the plain code when the
27
+ * extension is unknown or unsupported.
28
+ */
29
+ export declare function highlightByPath(code: string, filePath: string, theme: InkThemeAdapter): string;
30
+ /** Resolve a file path to a highlight.js language id, or null when unknown. */
31
+ export declare function languageFromPath(filePath: string): string | null;