@letta-ai/letta-code 0.5.0 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letta-ai/letta-code",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
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": {
@@ -2,9 +2,34 @@ import chalk from 'chalk';
2
2
  import { Text, useInput } from 'ink';
3
3
  import React, { useEffect, useState } from 'react';
4
4
 
5
+ /**
6
+ * Determines if the input should be treated as a control sequence (not inserted as text).
7
+ * This centralizes escape sequence filtering to prevent garbage characters from being inserted.
8
+ */
9
+ function isControlSequence(input, key) {
10
+ // Pasted content is handled separately
11
+ if (key?.isPasted) return true;
12
+
13
+ // Standard control keys (but NOT plain escape - apps may need it for shortcuts)
14
+ if (key.tab || (key.ctrl && input === 'c')) return true;
15
+ if (key.shift && key.tab) return true;
16
+
17
+ // Ctrl+W (delete word) - handled by parent component
18
+ if (key.ctrl && (input === 'w' || input === 'W')) return true;
19
+
20
+ // Option+Arrow escape sequences: Ink parses \x1bb as meta=true, input='b'
21
+ if (key.meta && (input === 'b' || input === 'B' || input === 'f' || input === 'F')) return true;
22
+
23
+ // Filter specific escape sequences that would insert garbage, but allow plain ESC through
24
+ // CSI sequences (ESC[...), Option+Delete (ESC + DEL), and other multi-char escape sequences
25
+ if (input && typeof input === 'string' && input.startsWith('\x1b') && input.length > 1) return true;
26
+
27
+ return false;
28
+ }
29
+
5
30
  function TextInput({ value: originalValue, placeholder = '', focus = true, mask, highlightPastedText = false, showCursor = true, onChange, onSubmit, externalCursorOffset, onCursorOffsetChange }) {
6
- const [state, setState] = useState({ cursorOffset: (originalValue || '').length, cursorWidth: 0 });
7
- const { cursorOffset, cursorWidth } = state;
31
+ const [state, setState] = useState({ cursorOffset: (originalValue || '').length, cursorWidth: 0, killBuffer: '' });
32
+ const { cursorOffset, cursorWidth, killBuffer } = state;
8
33
  useEffect(() => {
9
34
  setState(previousState => {
10
35
  if (!focus || !showCursor) {
@@ -12,7 +37,7 @@ function TextInput({ value: originalValue, placeholder = '', focus = true, mask,
12
37
  }
13
38
  const newValue = originalValue || '';
14
39
  if (previousState.cursorOffset > newValue.length - 1) {
15
- return { cursorOffset: newValue.length, cursorWidth: 0 };
40
+ return { ...previousState, cursorOffset: newValue.length, cursorWidth: 0 };
16
41
  }
17
42
  return previousState;
18
43
  });
@@ -21,7 +46,7 @@ function TextInput({ value: originalValue, placeholder = '', focus = true, mask,
21
46
  if (typeof externalCursorOffset === 'number') {
22
47
  const newValue = originalValue || '';
23
48
  const clamped = Math.max(0, Math.min(externalCursorOffset, newValue.length));
24
- setState(prev => ({ cursorOffset: clamped, cursorWidth: 0 }));
49
+ setState(prev => ({ ...prev, cursorOffset: clamped, cursorWidth: 0 }));
25
50
  if (typeof onCursorOffsetChange === 'function') onCursorOffsetChange(clamped);
26
51
  }
27
52
  }, [externalCursorOffset, originalValue, onCursorOffsetChange]);
@@ -42,11 +67,8 @@ function TextInput({ value: originalValue, placeholder = '', focus = true, mask,
42
67
  }
43
68
  }
44
69
  useInput((input, key) => {
45
- if (key && key.isPasted) {
46
- return;
47
- }
48
- // Treat Escape as a control key (don't insert into value)
49
- if (key.escape || (key.ctrl && input === 'c') || key.tab || (key.shift && key.tab)) {
70
+ // Filter control sequences (escape keys, Option+Arrow garbage, etc.)
71
+ if (isControlSequence(input, key)) {
50
72
  return;
51
73
  }
52
74
  if (key.return) {
@@ -58,22 +80,25 @@ function TextInput({ value: originalValue, placeholder = '', focus = true, mask,
58
80
  let nextCursorOffset = cursorOffset;
59
81
  let nextValue = originalValue;
60
82
  let nextCursorWidth = 0;
61
- if (key.leftArrow) {
62
- if (showCursor) {
63
- nextCursorOffset--;
83
+ let nextKillBuffer = killBuffer;
84
+ if (key.leftArrow || key.rightArrow) {
85
+ // Skip if meta is pressed - Option+Arrow is handled by parent for word navigation
86
+ if (key.meta) {
87
+ return;
64
88
  }
65
- }
66
- else if (key.rightArrow) {
67
89
  if (showCursor) {
68
- nextCursorOffset++;
90
+ nextCursorOffset += key.leftArrow ? -1 : 1;
69
91
  }
70
92
  }
71
93
  else if (key.upArrow || key.downArrow) {
72
- // Handle wrapped line navigation - don't handle here, let parent decide
73
- // Parent will check cursor position to determine if at boundary
94
+ // Let parent decide (wrapped line navigation)
74
95
  return;
75
96
  }
76
97
  else if (key.backspace || key.delete) {
98
+ // Skip if meta is pressed - Option+Delete is handled by parent for word deletion
99
+ if (key.meta) {
100
+ return;
101
+ }
77
102
  if (cursorOffset > 0) {
78
103
  nextValue = originalValue.slice(0, cursorOffset - 1) + originalValue.slice(cursorOffset, originalValue.length);
79
104
  nextCursorOffset--;
@@ -91,6 +116,28 @@ function TextInput({ value: originalValue, placeholder = '', focus = true, mask,
91
116
  nextCursorOffset = originalValue.length;
92
117
  }
93
118
  }
119
+ else if (key.ctrl && input === 'k') {
120
+ // CTRL-K: kill from cursor to end of line
121
+ if (cursorOffset < originalValue.length) {
122
+ nextKillBuffer = originalValue.slice(cursorOffset);
123
+ nextValue = originalValue.slice(0, cursorOffset);
124
+ }
125
+ }
126
+ else if (key.ctrl && input === 'u') {
127
+ // CTRL-U: kill from beginning to cursor
128
+ if (cursorOffset > 0) {
129
+ nextKillBuffer = originalValue.slice(0, cursorOffset);
130
+ nextValue = originalValue.slice(cursorOffset);
131
+ nextCursorOffset = 0;
132
+ }
133
+ }
134
+ else if (key.ctrl && input === 'y') {
135
+ // CTRL-Y: yank (paste) from kill buffer
136
+ if (killBuffer) {
137
+ nextValue = originalValue.slice(0, cursorOffset) + killBuffer + originalValue.slice(cursorOffset);
138
+ nextCursorOffset = cursorOffset + killBuffer.length;
139
+ }
140
+ }
94
141
  else {
95
142
  nextValue = originalValue.slice(0, cursorOffset) + input + originalValue.slice(cursorOffset, originalValue.length);
96
143
  nextCursorOffset += input.length;
@@ -99,7 +146,7 @@ function TextInput({ value: originalValue, placeholder = '', focus = true, mask,
99
146
  }
100
147
  }
101
148
  nextCursorOffset = Math.max(0, Math.min(nextCursorOffset, nextValue.length));
102
- setState({ cursorOffset: nextCursorOffset, cursorWidth: nextCursorWidth });
149
+ setState(prev => ({ ...prev, cursorOffset: nextCursorOffset, cursorWidth: nextCursorWidth, killBuffer: nextKillBuffer }));
103
150
  if (typeof onCursorOffsetChange === 'function') onCursorOffsetChange(nextCursorOffset);
104
151
  if (nextValue !== originalValue) {
105
152
  onChange(nextValue);