@letta-ai/letta-code 0.24.12 → 0.25.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letta-ai/letta-code",
3
- "version": "0.24.12",
3
+ "version": "0.25.0",
4
4
  "description": "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -48,12 +48,15 @@
48
48
  "@vscode/ripgrep": "^1.17.0"
49
49
  },
50
50
  "devDependencies": {
51
+ "@ai-sdk/anthropic": "^3.0.73",
52
+ "@ai-sdk/openai": "^3.0.55",
51
53
  "@slack/bolt": "^4.7.0",
52
54
  "@types/bun": "^1.3.7",
53
55
  "@types/diff": "^8.0.0",
54
56
  "@types/picomatch": "^4.0.2",
55
57
  "@types/react": "^19.2.9",
56
58
  "@types/ws": "^8.18.1",
59
+ "ai": "^6.0.171",
57
60
  "diff": "^8.0.2",
58
61
  "grammy": "^1.42.0",
59
62
  "husky": "9.1.7",
@@ -1,12 +1,77 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { readdirSync, readFileSync } from "node:fs";
4
- import { join, relative } from "node:path";
3
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
4
+ import { dirname, join, relative, resolve } from "node:path";
5
+
6
+ const TESTS_DIR_ENV = "LETTA_MOCK_ISOLATION_TESTS_DIR";
5
7
 
6
8
  const rootDir = process.cwd();
7
- const testsDir = join(rootDir, "src", "tests");
9
+ const testsDir = process.env[TESTS_DIR_ENV]
10
+ ? resolve(process.env[TESTS_DIR_ENV])
11
+ : join(rootDir, "src", "tests");
12
+
13
+ const FORBIDDEN_MOCK_MODULES = new Map([
14
+ [
15
+ "/channels/config",
16
+ "Use __testOverrideChannelsRoot() instead of replacing the shared channel config module.",
17
+ ],
18
+ [
19
+ "/agent/context",
20
+ "Use explicit env/context override seams instead of mocking the shared agent context module.",
21
+ ],
22
+ [
23
+ "/runtime-context",
24
+ "Use RuntimeContextSnapshot builders/overrides instead of mocking the shared runtime context module.",
25
+ ],
26
+ [
27
+ "/settings-manager",
28
+ "Use settings temp files or test override helpers instead of mocking the singleton settings manager.",
29
+ ],
30
+ ]);
31
+
32
+ const COMPLETE_EXPORT_MOCK_MODULES = new Set([
33
+ "/channels/slack/runtime",
34
+ "/channels/telegram/runtime",
35
+ "/channels/discord/runtime",
36
+ ]);
37
+
38
+ // Existing top-level module mocks that predate this guard. New top-level
39
+ // internal mocks are rejected by default because they are active while Bun loads
40
+ // other test files in the same process. Prefer dependency injection or an
41
+ // explicit test override helper. If a new top-level mock is truly unavoidable,
42
+ // add a file+module entry here in the same PR with a clear explanation.
43
+ const ALLOWED_TOP_LEVEL_MOCKS = new Set([
44
+ "src/tests/channels/discord-registry.test.ts::../../backend/api/client",
45
+ "src/tests/channels/slack-adapter-interop.test.ts::../../channels/slack/media",
46
+ "src/tests/channels/slack-adapter-interop.test.ts::../../channels/slack/runtime",
47
+ "src/tests/channels/slack-adapter.test.ts::../../channels/slack/media",
48
+ "src/tests/channels/slack-adapter.test.ts::../../channels/slack/runtime",
49
+ "src/tests/channels/telegram-adapter.test.ts::../../channels/telegram/runtime",
50
+ "src/tests/cli/message-search-cache-warm.test.ts::../../backend/api/search",
51
+ "src/tests/hooks/prompt-executor.test.ts::../../backend/api/generate",
52
+ "src/tests/tools/memory-apply-patch.test.ts::../../backend/api/client",
53
+ "src/tests/tools/memory-tool.test.ts::../../backend/api/client",
54
+ "src/tests/tools/toolset-client-tool-rule-cleanup.test.ts::../../backend/api/client",
55
+ "src/tests/tools/toolset-memfs-detach.test.ts::../../backend/api/client",
56
+ "src/tests/websocket/listen-client-concurrency.test.ts::../../agent/approval-execution",
57
+ "src/tests/websocket/listen-client-concurrency.test.ts::../../agent/approval-recovery",
58
+ "src/tests/websocket/listen-client-concurrency.test.ts::../../agent/message",
59
+ "src/tests/websocket/listen-client-concurrency.test.ts::../../backend/api/client",
60
+ "src/tests/websocket/listen-client-concurrency.test.ts::../../cli/helpers/approvalClassification",
61
+ "src/tests/websocket/listen-client-concurrency.test.ts::../../cli/helpers/stream",
62
+ ]);
63
+
64
+ const mockModulePattern = /\bmock\.module\s*\(\s*(["'`])([^"'`]+)\1/g;
65
+ const restoreHookPattern =
66
+ /\bafter(?:All|Each)\s*\(\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>[\s\S]*?\bmock\.restore\s*\(/m;
67
+ const restoreHookFunctionPattern =
68
+ /\bafter(?:All|Each)\s*\(\s*function\b[\s\S]*?\bmock\.restore\s*\(/m;
8
69
 
9
70
  function collectTestFiles(dir) {
71
+ if (!existsSync(dir)) {
72
+ return [];
73
+ }
74
+
10
75
  const entries = readdirSync(dir, { withFileTypes: true });
11
76
  const files = [];
12
77
 
@@ -27,11 +92,259 @@ function collectTestFiles(dir) {
27
92
  return files;
28
93
  }
29
94
 
30
- const mockModulePattern = /\bmock\.module\s*\(\s*(["'`])([^"'`]+)\1/g;
31
- const restoreHookPattern =
32
- /\bafter(?:All|Each)\s*\(\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>[\s\S]*?\bmock\.restore\s*\(/m;
33
- const restoreHookFunctionPattern =
34
- /\bafter(?:All|Each)\s*\(\s*function\b[\s\S]*?\bmock\.restore\s*\(/m;
95
+ function normalizeModuleSpecifier(moduleSpecifier) {
96
+ return moduleSpecifier
97
+ .replaceAll("\\", "/")
98
+ .replace(/\.(?:ts|tsx|js|jsx)$/u, "");
99
+ }
100
+
101
+ function moduleMatches(moduleSpecifier, suffixes) {
102
+ const normalized = normalizeModuleSpecifier(moduleSpecifier);
103
+ for (const suffix of suffixes) {
104
+ if (normalized.endsWith(suffix)) {
105
+ return suffix;
106
+ }
107
+ }
108
+ return null;
109
+ }
110
+
111
+ function isRelativeInternalModule(moduleSpecifier) {
112
+ return moduleSpecifier.startsWith("../") || moduleSpecifier.startsWith("./");
113
+ }
114
+
115
+ function lineAndColumn(sourceText, index) {
116
+ const before = sourceText.slice(0, index);
117
+ const lines = before.split("\n");
118
+ return {
119
+ line: lines.length,
120
+ column: (lines.at(-1)?.length ?? 0) + 1,
121
+ };
122
+ }
123
+
124
+ function isTopLevelMockCall(sourceText, index) {
125
+ const lineStart = sourceText.lastIndexOf("\n", index - 1) + 1;
126
+ return lineStart === index;
127
+ }
128
+
129
+ function isLineCommentMatch(sourceText, index) {
130
+ const lineStart = sourceText.lastIndexOf("\n", index - 1) + 1;
131
+ const linePrefix = sourceText.slice(lineStart, index);
132
+ return linePrefix.includes("//");
133
+ }
134
+
135
+ function isInsideQuotedText(sourceText, index) {
136
+ let quote = null;
137
+ let escaped = false;
138
+
139
+ for (let i = 0; i < index; i += 1) {
140
+ const char = sourceText[i];
141
+
142
+ if (quote) {
143
+ if (escaped) {
144
+ escaped = false;
145
+ continue;
146
+ }
147
+ if (char === "\\") {
148
+ escaped = true;
149
+ continue;
150
+ }
151
+ if (char === quote) {
152
+ quote = null;
153
+ }
154
+ continue;
155
+ }
156
+
157
+ if (char === '"' || char === "'" || char === "`") {
158
+ quote = char;
159
+ }
160
+ }
161
+
162
+ return quote !== null;
163
+ }
164
+
165
+ function findMatchingParen(sourceText, openParenIndex) {
166
+ let depth = 0;
167
+ let quote = null;
168
+ let escaped = false;
169
+
170
+ for (let i = openParenIndex; i < sourceText.length; i += 1) {
171
+ const char = sourceText[i];
172
+
173
+ if (quote) {
174
+ if (escaped) {
175
+ escaped = false;
176
+ continue;
177
+ }
178
+ if (char === "\\") {
179
+ escaped = true;
180
+ continue;
181
+ }
182
+ if (char === quote) {
183
+ quote = null;
184
+ }
185
+ continue;
186
+ }
187
+
188
+ if (char === '"' || char === "'" || char === "`") {
189
+ quote = char;
190
+ continue;
191
+ }
192
+
193
+ if (char === "(") {
194
+ depth += 1;
195
+ continue;
196
+ }
197
+ if (char === ")") {
198
+ depth -= 1;
199
+ if (depth === 0) {
200
+ return i;
201
+ }
202
+ }
203
+ }
204
+
205
+ return -1;
206
+ }
207
+
208
+ function extractMockCallText(sourceText, index) {
209
+ const openParenIndex = sourceText.indexOf("(", index);
210
+ if (openParenIndex === -1) {
211
+ return "";
212
+ }
213
+ const closeParenIndex = findMatchingParen(sourceText, openParenIndex);
214
+ if (closeParenIndex === -1) {
215
+ return sourceText.slice(index);
216
+ }
217
+ return sourceText.slice(index, closeParenIndex + 1);
218
+ }
219
+
220
+ function resolveMockTargetPath(filePath, moduleSpecifier) {
221
+ const basePath = resolve(dirname(filePath), moduleSpecifier);
222
+ const candidates = [
223
+ basePath,
224
+ `${basePath}.ts`,
225
+ `${basePath}.tsx`,
226
+ `${basePath}.js`,
227
+ `${basePath}.jsx`,
228
+ join(basePath, "index.ts"),
229
+ join(basePath, "index.tsx"),
230
+ join(basePath, "index.js"),
231
+ join(basePath, "index.jsx"),
232
+ ];
233
+ return candidates.find((candidate) => existsSync(candidate)) ?? null;
234
+ }
235
+
236
+ function getExportedNames(filePath) {
237
+ const sourceText = readFileSync(filePath, "utf8");
238
+ const exportedNames = new Set();
239
+
240
+ for (const match of sourceText.matchAll(
241
+ /\bexport\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/g,
242
+ )) {
243
+ if (match[1]) exportedNames.add(match[1]);
244
+ }
245
+ for (const match of sourceText.matchAll(
246
+ /\bexport\s+(?:const|let|var|class)\s+([A-Za-z_$][\w$]*)/g,
247
+ )) {
248
+ if (match[1]) exportedNames.add(match[1]);
249
+ }
250
+ for (const match of sourceText.matchAll(/\bexport\s*{([^}]+)}/g)) {
251
+ const specifiers = match[1]?.split(",") ?? [];
252
+ for (const specifier of specifiers) {
253
+ const cleaned = specifier.trim();
254
+ if (!cleaned || cleaned.startsWith("type ")) continue;
255
+ const aliasMatch = cleaned.match(/\bas\s+([A-Za-z_$][\w$]*)$/);
256
+ const nameMatch = cleaned.match(/^(?:type\s+)?([A-Za-z_$][\w$]*)/);
257
+ const exportedName = aliasMatch?.[1] ?? nameMatch?.[1];
258
+ if (exportedName) exportedNames.add(exportedName);
259
+ }
260
+ }
261
+
262
+ return exportedNames;
263
+ }
264
+
265
+ function getMockedObjectKeys(mockCallText) {
266
+ const keys = new Set();
267
+ const arrowObjectIndex = mockCallText.indexOf("=> ({");
268
+ const objectStartIndex =
269
+ arrowObjectIndex === -1
270
+ ? mockCallText.indexOf("return {")
271
+ : mockCallText.indexOf("{", arrowObjectIndex);
272
+ if (objectStartIndex === -1) {
273
+ return keys;
274
+ }
275
+
276
+ let depth = 0;
277
+ let quote = null;
278
+ let escaped = false;
279
+ let keyCandidate = "";
280
+ let readingKey = false;
281
+
282
+ for (let i = objectStartIndex; i < mockCallText.length; i += 1) {
283
+ const char = mockCallText[i];
284
+
285
+ if (quote) {
286
+ if (escaped) {
287
+ escaped = false;
288
+ continue;
289
+ }
290
+ if (char === "\\") {
291
+ escaped = true;
292
+ continue;
293
+ }
294
+ if (char === quote) {
295
+ quote = null;
296
+ }
297
+ continue;
298
+ }
299
+
300
+ if (char === '"' || char === "'" || char === "`") {
301
+ quote = char;
302
+ continue;
303
+ }
304
+
305
+ if (char === "{") {
306
+ depth += 1;
307
+ readingKey = depth === 1;
308
+ keyCandidate = "";
309
+ continue;
310
+ }
311
+ if (char === "}") {
312
+ depth -= 1;
313
+ readingKey = depth === 1;
314
+ keyCandidate = "";
315
+ if (depth <= 0) break;
316
+ continue;
317
+ }
318
+
319
+ if (depth !== 1) {
320
+ continue;
321
+ }
322
+
323
+ if (char === ",") {
324
+ readingKey = true;
325
+ keyCandidate = "";
326
+ continue;
327
+ }
328
+
329
+ if (!readingKey) {
330
+ continue;
331
+ }
332
+
333
+ if (char === ":") {
334
+ const key = keyCandidate.trim();
335
+ if (/^[A-Za-z_$][\w$]*$/u.test(key)) {
336
+ keys.add(key);
337
+ }
338
+ readingKey = false;
339
+ keyCandidate = "";
340
+ continue;
341
+ }
342
+
343
+ keyCandidate += char;
344
+ }
345
+
346
+ return keys;
347
+ }
35
348
 
36
349
  const failures = [];
37
350
 
@@ -39,40 +352,143 @@ for (const filePath of collectTestFiles(testsDir)) {
39
352
  const sourceText = readFileSync(filePath, "utf8");
40
353
  const mockedModules = Array.from(
41
354
  sourceText.matchAll(mockModulePattern),
42
- (match) => match[2] ?? "<dynamic module>",
355
+ ).filter(
356
+ (match) =>
357
+ !isLineCommentMatch(sourceText, match.index ?? 0) &&
358
+ !isInsideQuotedText(sourceText, match.index ?? 0),
43
359
  );
44
360
 
45
361
  if (mockedModules.length === 0) continue;
46
362
 
363
+ const relativePath = relative(rootDir, filePath).replaceAll("\\", "/");
47
364
  const hasRestoreHook =
48
365
  restoreHookPattern.test(sourceText) ||
49
366
  restoreHookFunctionPattern.test(sourceText);
50
- if (hasRestoreHook) continue;
367
+ if (!hasRestoreHook) {
368
+ failures.push({
369
+ type: "missing-restore",
370
+ filePath: relativePath,
371
+ mockedModules: mockedModules.map(
372
+ (match) => match[2] ?? "<dynamic module>",
373
+ ),
374
+ });
375
+ }
376
+
377
+ for (const match of mockedModules) {
378
+ const moduleSpecifier = match[2] ?? "<dynamic module>";
379
+ const location = lineAndColumn(sourceText, match.index ?? 0);
380
+ const forbiddenSuffix = moduleMatches(
381
+ moduleSpecifier,
382
+ FORBIDDEN_MOCK_MODULES.keys(),
383
+ );
384
+ if (forbiddenSuffix) {
385
+ failures.push({
386
+ type: "forbidden-module",
387
+ filePath: relativePath,
388
+ location,
389
+ moduleSpecifier,
390
+ reason: FORBIDDEN_MOCK_MODULES.get(forbiddenSuffix),
391
+ });
392
+ }
393
+
394
+ if (
395
+ isRelativeInternalModule(moduleSpecifier) &&
396
+ isTopLevelMockCall(sourceText, match.index ?? 0)
397
+ ) {
398
+ const key = `${relativePath}::${moduleSpecifier}`;
399
+ if (!ALLOWED_TOP_LEVEL_MOCKS.has(key)) {
400
+ failures.push({
401
+ type: "top-level-mock",
402
+ filePath: relativePath,
403
+ location,
404
+ moduleSpecifier,
405
+ });
406
+ }
407
+ }
408
+
409
+ const completeMockSuffix = moduleMatches(
410
+ moduleSpecifier,
411
+ COMPLETE_EXPORT_MOCK_MODULES,
412
+ );
413
+ if (completeMockSuffix) {
414
+ const targetPath = resolveMockTargetPath(filePath, moduleSpecifier);
415
+ if (!targetPath) continue;
51
416
 
52
- failures.push({
53
- filePath,
54
- mockedModules,
55
- });
417
+ const exportedNames = getExportedNames(targetPath);
418
+ const mockCallText = extractMockCallText(sourceText, match.index ?? 0);
419
+ const mockedKeys = getMockedObjectKeys(mockCallText);
420
+ const missingExports = [...exportedNames].filter(
421
+ (exportedName) => !mockedKeys.has(exportedName),
422
+ );
423
+ if (missingExports.length > 0) {
424
+ failures.push({
425
+ type: "partial-runtime-mock",
426
+ filePath: relativePath,
427
+ location,
428
+ moduleSpecifier,
429
+ missingExports,
430
+ });
431
+ }
432
+ }
433
+ }
56
434
  }
57
435
 
58
436
  if (failures.length > 0) {
59
- console.error(
60
- "❌ Found test files that call mock.module() without a top-level afterEach/afterAll hook that calls mock.restore().\n",
61
- );
437
+ console.error("❌ Found unsafe Bun mock.module() usage.\n");
62
438
 
63
439
  for (const failure of failures) {
64
- const relPath = relative(rootDir, failure.filePath);
65
- console.error(`- ${relPath}`);
66
- console.error(` mocked modules: ${failure.mockedModules.join(", ")}`);
440
+ switch (failure.type) {
441
+ case "missing-restore":
442
+ console.error(`- ${failure.filePath}`);
443
+ console.error(
444
+ " missing: top-level afterEach/afterAll mock.restore() hook",
445
+ );
446
+ console.error(` mocked modules: ${failure.mockedModules.join(", ")}`);
447
+ break;
448
+ case "forbidden-module":
449
+ console.error(
450
+ `- ${failure.filePath}:${failure.location.line}:${failure.location.column}`,
451
+ );
452
+ console.error(
453
+ ` forbidden shared module mock: ${failure.moduleSpecifier}`,
454
+ );
455
+ console.error(` ${failure.reason}`);
456
+ break;
457
+ case "top-level-mock":
458
+ console.error(
459
+ `- ${failure.filePath}:${failure.location.line}:${failure.location.column}`,
460
+ );
461
+ console.error(
462
+ ` unsafe top-level internal module mock: ${failure.moduleSpecifier}`,
463
+ );
464
+ console.error(
465
+ " Top-level mock.module() calls are active while Bun loads other test files. Move the mock into the test/beforeEach, use dependency injection, or add an explicit test override helper.",
466
+ );
467
+ break;
468
+ case "partial-runtime-mock":
469
+ console.error(
470
+ `- ${failure.filePath}:${failure.location.line}:${failure.location.column}`,
471
+ );
472
+ console.error(
473
+ ` partial channel runtime mock: ${failure.moduleSpecifier}`,
474
+ );
475
+ console.error(
476
+ ` missing exports: ${failure.missingExports.join(", ")}`,
477
+ );
478
+ console.error(
479
+ " Runtime module mocks must include every runtime export so later imports do not fail with missing ESM exports.",
480
+ );
481
+ break;
482
+ }
67
483
  }
68
484
 
69
485
  console.error(
70
486
  "\nWhy this fails: Bun module mocks are process-global and can leak across files in the shared module cache.",
71
487
  );
72
488
  console.error(
73
- "Add a top-level afterEach(() => { mock.restore(); }) or afterAll(() => { mock.restore(); }) hook, or remove the module mock in favor of dependency injection.",
489
+ "Prefer explicit test override helpers or dependency injection. If a module mock is unavoidable, keep it scoped and restore it with afterEach().",
74
490
  );
75
491
  process.exit(1);
76
492
  }
77
493
 
78
- console.log("✅ No unguarded mock.module() usage found.");
494
+ console.log("✅ No unsafe mock.module() usage found.");
package/scripts/check.js CHANGED
@@ -15,7 +15,7 @@ try {
15
15
  } catch (error) {
16
16
  console.error("❌ Mock isolation check failed\n");
17
17
  console.error(
18
- "Add a top-level mock.restore() teardown hook to any test file using mock.module(), or remove the module mock.\n",
18
+ "Fix the unsafe mock.module() usage above. Prefer explicit test override helpers or scoped mocks with afterEach(mock.restore).\n",
19
19
  );
20
20
  failed = true;
21
21
  }
@@ -1,7 +1,14 @@
1
1
  import chalk from 'chalk';
2
- import { Text, useInput } from 'ink';
2
+ import { Text, Transform, useInput } from 'ink';
3
3
  import React, { useEffect, useState } from 'react';
4
4
 
5
+ // Use a private-use sentinel while Ink/wrap-ansi measure and wrap text.
6
+ // wrap-ansi with trim:true strips inverse ASCII spaces at line boundaries, but
7
+ // rendering an inverse NBSP can show a visible glyph in some terminals/fonts.
8
+ // The Transform below converts this sentinel back to an inverse ASCII space
9
+ // after wrapping has completed, before anything is written to the terminal.
10
+ const CURSOR_SENTINEL = '\u{10FFFD}';
11
+
5
12
  /**
6
13
  * Determines if the input should be treated as a control sequence (not inserted as text).
7
14
  * This centralizes escape sequence filtering to prevent garbage characters from being inserted.
@@ -70,21 +77,21 @@ function TextInput({ value: originalValue, placeholder = '', focus = true, mask,
70
77
  let renderedValue = value;
71
78
  let renderedPlaceholder = placeholder ? chalk.grey(placeholder) : undefined;
72
79
  if (showCursor && focus) {
73
- renderedPlaceholder = placeholder.length > 0 ? chalk.inverse(placeholder[0]) + chalk.grey(placeholder.slice(1)) : chalk.inverse('\u00A0');
74
- renderedValue = value.length > 0 ? '' : chalk.inverse('\u00A0');
80
+ renderedPlaceholder = placeholder.length > 0 ? chalk.inverse(placeholder[0]) + chalk.grey(placeholder.slice(1)) : CURSOR_SENTINEL;
81
+ renderedValue = value.length > 0 ? '' : CURSOR_SENTINEL;
75
82
  let i = 0;
76
83
  for (const char of value) {
77
84
  const isCursorPosition = i >= cursorOffset - cursorActualWidth && i <= cursorOffset;
78
85
  if (isCursorPosition && char === '\n') {
79
86
  // Newline at cursor: show inverted space (visible cursor) then the newline
80
- renderedValue += chalk.inverse('\u00A0') + char;
87
+ renderedValue += CURSOR_SENTINEL + char;
81
88
  } else {
82
89
  renderedValue += isCursorPosition ? chalk.inverse(char) : char;
83
90
  }
84
91
  i++;
85
92
  }
86
93
  if (value.length > 0 && cursorOffset === value.length) {
87
- renderedValue += chalk.inverse('\u00A0');
94
+ renderedValue += CURSOR_SENTINEL;
88
95
  }
89
96
  }
90
97
  useInput((input, key) => {
@@ -173,7 +180,8 @@ function TextInput({ value: originalValue, placeholder = '', focus = true, mask,
173
180
  onChange(nextValue);
174
181
  }
175
182
  }, { isActive: focus });
176
- return (React.createElement(Text, null, placeholder ? (value.length > 0 ? renderedValue : renderedPlaceholder) : renderedValue));
183
+ return (React.createElement(Transform, { transform: line => line.replaceAll(CURSOR_SENTINEL, chalk.inverse(' ')) },
184
+ React.createElement(Text, null, placeholder ? (value.length > 0 ? renderedValue : renderedPlaceholder) : renderedValue)));
177
185
  }
178
186
  export default TextInput;
179
187
  export function UncontrolledTextInput({ initialValue = '', ...props }) {