@iaforged/context-code 1.1.4 → 1.1.7

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 (34) hide show
  1. package/README.md +32 -8
  2. package/dist/src/commands/init.js +91 -219
  3. package/dist/src/commands/voice/index.js +6 -7
  4. package/dist/src/commands/voice/voice.js +87 -43
  5. package/dist/src/commands.js +1 -3
  6. package/dist/src/components/LogoV2/VoiceModeNotice.js +1 -1
  7. package/dist/src/components/PromptInput/VoiceIndicator.js +4 -4
  8. package/dist/src/components/Spinner.js +18 -18
  9. package/dist/src/constants/spinnerVerbs.js +9 -9
  10. package/dist/src/hooks/usePasteHandler.js +8 -8
  11. package/dist/src/hooks/useVoice.js +93 -805
  12. package/dist/src/hooks/useVoiceEnabled.js +3 -15
  13. package/dist/src/hooks/useVoiceIntegration.js +6 -25
  14. package/dist/src/keybindings/defaultBindings.js +9 -6
  15. package/dist/src/screens/REPL.js +10 -22
  16. package/dist/src/services/localDictation.js +520 -0
  17. package/dist/src/services/voice.js +9 -7
  18. package/dist/src/state/AppState.js +1 -3
  19. package/dist/src/tools/ConfigTool/ConfigTool.js +12 -15
  20. package/dist/src/tools/ConfigTool/supportedSettings.js +2 -2
  21. package/dist/src/utils/imagePaste.js +11 -5
  22. package/dist/src/utils/model/model.js +2 -0
  23. package/dist/src/utils/settings/types.js +2 -2
  24. package/dist/src/voice/voiceModeEnabled.js +5 -25
  25. package/dist/vendor/audio-capture/arm64-darwin/audio-capture.node +0 -0
  26. package/dist/vendor/audio-capture/arm64-linux/audio-capture.node +0 -0
  27. package/dist/vendor/audio-capture/arm64-win32/audio-capture.node +0 -0
  28. package/dist/vendor/audio-capture/x64-darwin/audio-capture.node +0 -0
  29. package/dist/vendor/audio-capture/x64-linux/audio-capture.node +0 -0
  30. package/dist/vendor/audio-capture/x64-win32/audio-capture.node +0 -0
  31. package/dist/vendor/audio-capture-src/index.js +114 -0
  32. package/dist/vendor/audio-capture-src/index.ts +155 -0
  33. package/docs/comandos.md +132 -121
  34. package/package.json +1 -1
@@ -1,31 +1,84 @@
1
1
  import { normalizeLanguageForSTT } from '../../hooks/useVoice.js';
2
2
  import { getShortcutDisplay } from '../../keybindings/shortcutFormat.js';
3
3
  import { logEvent } from '../../services/analytics/index.js';
4
- import { isAnthropicAuthEnabled } from '../../utils/auth.js';
4
+ import { checkLocalDictationConfiguration, getLocalDictationStatus, installLocalDictation, } from '../../services/localDictation.js';
5
5
  import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
6
6
  import { settingsChangeDetector } from '../../utils/settings/changeDetector.js';
7
7
  import { getInitialSettings, updateSettingsForSource, } from '../../utils/settings/settings.js';
8
8
  import { isVoiceModeEnabled } from '../../voice/voiceModeEnabled.js';
9
9
  const LANG_HINT_MAX_SHOWS = 2;
10
- export const call = async () => {
11
- // Check auth and kill-switch before allowing voice mode
10
+ function parseArgs(args) {
11
+ const tokens = args.trim().split(/\s+/).filter(Boolean);
12
+ if (tokens.length === 0) {
13
+ return { action: 'toggle' };
14
+ }
15
+ const [command, ...rest] = tokens;
16
+ switch (command.toLowerCase()) {
17
+ case 'install':
18
+ case 'instalar':
19
+ return { action: 'install', value: rest.join(' ').trim() || undefined };
20
+ case 'status':
21
+ case 'estado':
22
+ return { action: 'status' };
23
+ case 'help':
24
+ case 'ayuda':
25
+ return { action: 'help' };
26
+ default:
27
+ return { action: 'help' };
28
+ }
29
+ }
30
+ function getUsageText() {
31
+ return [
32
+ 'Uso de /dictar:',
33
+ '/dictar',
34
+ '/dictar install [modelo]',
35
+ '/dictar status',
36
+ '',
37
+ 'Modelo por defecto: base (multidioma).',
38
+ 'Ejemplos:',
39
+ '- /dictar install',
40
+ '- /dictar install base',
41
+ ].join('\n');
42
+ }
43
+ export const call = async (args) => {
12
44
  if (!isVoiceModeEnabled()) {
13
- // Differentiate: OAuth-less users get an auth hint, everyone else
14
- // gets nothing (command shouldn't be reachable when the kill-switch is on).
15
- if (!isAnthropicAuthEnabled()) {
45
+ return {
46
+ type: 'text',
47
+ value: 'El modo de dictado no esta disponible.',
48
+ };
49
+ }
50
+ const parsed = parseArgs(args);
51
+ if (parsed.action === 'help') {
52
+ return { type: 'text', value: getUsageText() };
53
+ }
54
+ if (parsed.action === 'status') {
55
+ return {
56
+ type: 'text',
57
+ value: await getLocalDictationStatus(),
58
+ };
59
+ }
60
+ if (parsed.action === 'install') {
61
+ try {
62
+ const result = await installLocalDictation(parsed.value);
16
63
  return {
17
64
  type: 'text',
18
- value: 'Voice mode requires a Claude.ai account. Please run /login to sign in.',
65
+ value: `Instalacion completada.\n` +
66
+ `Backend: ${result.executablePath}\n` +
67
+ `Modelo: ${result.modelPath}\n` +
68
+ `Release: ${result.releaseTag}\n\n` +
69
+ `Ahora ejecuta /dictar para activarlo.`,
70
+ };
71
+ }
72
+ catch (error) {
73
+ return {
74
+ type: 'text',
75
+ value: `No se pudo instalar el backend de dictado.\n` +
76
+ `${error instanceof Error ? error.message : String(error)}`,
19
77
  };
20
78
  }
21
- return {
22
- type: 'text',
23
- value: 'Voice mode is not available.',
24
- };
25
79
  }
26
80
  const currentSettings = getInitialSettings();
27
81
  const isCurrentlyEnabled = currentSettings.voiceEnabled === true;
28
- // Toggle OFF — no checks needed
29
82
  if (isCurrentlyEnabled) {
30
83
  const result = updateSettingsForSource('userSettings', {
31
84
  voiceEnabled: false,
@@ -33,70 +86,63 @@ export const call = async () => {
33
86
  if (result.error) {
34
87
  return {
35
88
  type: 'text',
36
- value: 'Failed to update settings. Check your settings file for syntax errors.',
89
+ value: 'No se pudo actualizar la configuracion. Revisa tu archivo de settings.',
37
90
  };
38
91
  }
39
92
  settingsChangeDetector.notifyChange('userSettings');
40
93
  logEvent('tengu_voice_toggled', { enabled: false });
41
94
  return {
42
95
  type: 'text',
43
- value: 'Voice mode disabled.',
96
+ value: 'Modo de dictado desactivado.',
44
97
  };
45
98
  }
46
- // Toggle ON run pre-flight checks first
47
- const { isVoiceStreamAvailable } = await import('../../services/voiceStreamSTT.js');
48
- const { checkRecordingAvailability } = await import('../../services/voice.js');
49
- // Check recording availability (microphone access)
50
- const recording = await checkRecordingAvailability();
51
- if (!recording.available) {
99
+ const dictation = await checkLocalDictationConfiguration();
100
+ if (!dictation.available) {
52
101
  return {
53
102
  type: 'text',
54
- value: recording.reason ?? 'Voice mode is not available in this environment.',
103
+ value: dictation.error ?? 'El dictado local no esta configurado.',
55
104
  };
56
105
  }
57
- // Check for API key
58
- if (!isVoiceStreamAvailable()) {
106
+ const { checkRecordingAvailability, checkVoiceDependencies, requestMicrophonePermission } = await import('../../services/voice.js');
107
+ const recording = await checkRecordingAvailability();
108
+ if (!recording.available) {
59
109
  return {
60
110
  type: 'text',
61
- value: 'Voice mode requires a Claude.ai account. Please run /login to sign in.',
111
+ value: recording.reason ??
112
+ 'El modo de dictado no esta disponible en este entorno.',
62
113
  };
63
114
  }
64
- // Check for recording tools
65
- const { checkVoiceDependencies, requestMicrophonePermission } = await import('../../services/voice.js');
66
115
  const deps = await checkVoiceDependencies();
67
116
  if (!deps.available) {
68
117
  const hint = deps.installCommand
69
- ? `\nInstall audio recording tools? Run: ${deps.installCommand}`
70
- : '\nInstall SoX manually for audio recording.';
118
+ ? `\nInstala las herramientas de audio con: ${deps.installCommand}`
119
+ : '\nInstala manualmente las herramientas de grabacion de audio.';
71
120
  return {
72
121
  type: 'text',
73
- value: `No audio recording tool found.${hint}`,
122
+ value: `No se encontro una herramienta de grabacion de audio.${hint}`,
74
123
  };
75
124
  }
76
- // Probe mic access so the OS permission dialog fires now rather than
77
- // on the user's first hold-to-talk activation.
78
125
  if (!(await requestMicrophonePermission())) {
79
126
  let guidance;
80
127
  if (process.platform === 'win32') {
81
- guidance = 'Settings \u2192 Privacy \u2192 Microphone';
128
+ guidance = 'Configuracion > Privacidad > Microfono';
82
129
  }
83
130
  else if (process.platform === 'linux') {
84
- guidance = "your system's audio settings";
131
+ guidance = 'la configuracion de audio del sistema';
85
132
  }
86
133
  else {
87
- guidance = 'System Settings \u2192 Privacy & Security \u2192 Microphone';
134
+ guidance = 'System Settings > Privacy & Security > Microphone';
88
135
  }
89
136
  return {
90
137
  type: 'text',
91
- value: `Microphone access is denied. To enable it, go to ${guidance}, then run /voice again.`,
138
+ value: `El acceso al microfono esta denegado. Habilitalo en ${guidance} y luego ejecuta /dictar otra vez.`,
92
139
  };
93
140
  }
94
- // All checks passed — enable voice
95
141
  const result = updateSettingsForSource('userSettings', { voiceEnabled: true });
96
142
  if (result.error) {
97
143
  return {
98
144
  type: 'text',
99
- value: 'Failed to update settings. Check your settings file for syntax errors.',
145
+ value: 'No se pudo actualizar la configuracion. Revisa tu archivo de settings.',
100
146
  };
101
147
  }
102
148
  settingsChangeDetector.notifyChange('userSettings');
@@ -104,17 +150,15 @@ export const call = async () => {
104
150
  const key = getShortcutDisplay('voice:pushToTalk', 'Chat', 'Space');
105
151
  const stt = normalizeLanguageForSTT(currentSettings.language);
106
152
  const cfg = getGlobalConfig();
107
- // Reset the hint counter whenever the resolved STT language changes
108
- // (including first-ever enable, where lastLanguage is undefined).
109
153
  const langChanged = cfg.voiceLangHintLastLanguage !== stt.code;
110
154
  const priorCount = langChanged ? 0 : (cfg.voiceLangHintShownCount ?? 0);
111
- const showHint = !stt.fellBackFrom && priorCount < LANG_HINT_MAX_SHOWS;
155
+ const showHint = priorCount < LANG_HINT_MAX_SHOWS;
112
156
  let langNote = '';
113
157
  if (stt.fellBackFrom) {
114
- langNote = ` Note: "${stt.fellBackFrom}" is not a supported dictation language; using English. Change it via /config.`;
158
+ langNote = ` Nota: "${stt.fellBackFrom}" no esta soportado por el backend local; se usara deteccion automatica.`;
115
159
  }
116
160
  else if (showHint) {
117
- langNote = ` Dictation language: ${stt.code} (/config to change).`;
161
+ langNote = ` Idioma de dictado: ${stt.code} (/config para cambiarlo).`;
118
162
  }
119
163
  if (langChanged || showHint) {
120
164
  saveGlobalConfig(prev => ({
@@ -125,6 +169,6 @@ export const call = async () => {
125
169
  }
126
170
  return {
127
171
  type: 'text',
128
- value: `Voice mode enabled. Hold ${key} to record.${langNote}`,
172
+ value: `Modo de dictado activado. Manten ${key} presionado para grabar.${langNote}`,
129
173
  };
130
174
  };
@@ -77,9 +77,7 @@ const bridge = feature('BRIDGE_MODE')
77
77
  const remoteControlServerCommand = feature('DAEMON') && feature('BRIDGE_MODE')
78
78
  ? require('./commands/remoteControlServer/index.js').default
79
79
  : null;
80
- const voiceCommand = feature('VOICE_MODE')
81
- ? require('./commands/voice/index.js').default
82
- : null;
80
+ const voiceCommand = require('./commands/voice/index.js').default;
83
81
  const forceSnip = feature('HISTORY_SNIP')
84
82
  ? require('./commands/force-snip.js').default
85
83
  : null;
@@ -57,7 +57,7 @@ function VoiceModeNoticeInner() {
57
57
  }
58
58
  let t2;
59
59
  if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
60
- t2 = _jsxs(Box, { paddingLeft: 2, children: [_jsx(AnimatedAsterisk, {}), _jsx(Text, { dimColor: true, children: " Voice mode is now available \u00B7 /voice to enable" })] });
60
+ t2 = _jsxs(Box, { paddingLeft: 2, children: [_jsx(AnimatedAsterisk, {}), _jsx(Text, { dimColor: true, children: " El dictado local ya esta disponible \u00B7 usa /dictar install para prepararlo o /dictar para activarlo" })] });
61
61
  $[3] = t2;
62
62
  }
63
63
  else {
@@ -40,7 +40,7 @@ function VoiceIndicatorImpl(t0) {
40
40
  {
41
41
  let t1;
42
42
  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
43
- t1 = _jsx(Text, { dimColor: true, children: "listening\u2026" });
43
+ t1 = _jsx(Text, { dimColor: true, children: "dictando..." });
44
44
  $[0] = t1;
45
45
  }
46
46
  else {
@@ -77,7 +77,7 @@ export function VoiceWarmupHint() {
77
77
  }
78
78
  let t0;
79
79
  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
80
- t0 = _jsx(Text, { dimColor: true, children: "keep holding\u2026" });
80
+ t0 = _jsx(Text, { dimColor: true, children: "sigue manteniendo..." });
81
81
  $[0] = t0;
82
82
  }
83
83
  else {
@@ -93,7 +93,7 @@ function ProcessingShimmer() {
93
93
  if (reducedMotion) {
94
94
  let t0;
95
95
  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
96
- t0 = _jsx(Text, { color: "warning", children: "Voice: processing\u2026" });
96
+ t0 = _jsx(Text, { color: "warning", children: "Dictado: procesando..." });
97
97
  $[0] = t0;
98
98
  }
99
99
  else {
@@ -115,7 +115,7 @@ function ProcessingShimmer() {
115
115
  const color = t0;
116
116
  let t1;
117
117
  if ($[3] !== color) {
118
- t1 = _jsx(Text, { color: color, children: "Voice: processing\u2026" });
118
+ t1 = _jsx(Text, { color: color, children: "Dictado: procesando..." });
119
119
  $[3] = color;
120
120
  $[4] = t1;
121
121
  }
@@ -39,11 +39,11 @@ const SPINNER_FRAMES = [...DEFAULT_CHARACTERS, ...[...DEFAULT_CHARACTERS].revers
39
39
  function translateSpinnerTip(tip) {
40
40
  if (tip ===
41
41
  'Double-tap esc to rewind the code and/or conversation to a previous point in time') {
42
- return 'Pulsa dos veces Esc para rebobinar el código y/o la conversación a un punto anterior en el tiempo';
42
+ return 'Pulsa dos veces Esc para rebobinar el codigo y/o la conversacion a un punto anterior en el tiempo';
43
43
  }
44
44
  if (tip ===
45
45
  'Double-tap esc to rewind the conversation to a previous point in time') {
46
- return 'Pulsa dos veces Esc para rebobinar la conversación a un punto anterior en el tiempo';
46
+ return 'Pulsa dos veces Esc para rebobinar la conversacion a un punto anterior en el tiempo';
47
47
  }
48
48
  return tip;
49
49
  }
@@ -52,18 +52,18 @@ function translateSpinnerTip(tip) {
52
52
  // violate Rules of Hooks (the inner variant calls ~10 more hooks).
53
53
  export function SpinnerWithVerb(props) {
54
54
  const isBriefOnly = useAppState(s => s.isBriefOnly);
55
- // REPL overrides isBriefOnlyfalse when viewing a teammate transcript
55
+ // REPL overrides isBriefOnly→false when viewing a teammate transcript
56
56
  // (see isBriefOnly={viewedTeammateTask ? false : isBriefOnly}). That
57
- // prop isn't threaded here, so replicate the gate from the store
57
+ // prop isn't threaded here, so replicate the gate from the store —
58
58
  // teammate view needs the real spinner (which shows teammate status).
59
59
  const viewingAgentTaskId = useAppState(s_0 => s_0.viewingAgentTaskId);
60
- // Hoisted to mount-time this component re-renders at animation framerate.
60
+ // Hoisted to mount-time — this component re-renders at animation framerate.
61
61
  const briefEnvEnabled = feature('KAIROS') || feature('KAIROS_BRIEF') ?
62
62
  // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
63
63
  useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_BRIEF), []) : false;
64
- // Runtime gate mirrors isBriefEnabled() but inlined importing from
64
+ // Runtime gate mirrors isBriefEnabled() but inlined — importing from
65
65
  // BriefTool.ts would leak tool-name strings into external builds. Single
66
- // spinner instance hooks stay unconditional (two subs, negligible).
66
+ // spinner instance → hooks stay unconditional (two subs, negligible).
67
67
  if ((feature('KAIROS') || feature('KAIROS_BRIEF')) && (getKairosActive() || getUserMsgOptIn() && (briefEnvEnabled || getFeatureValue_CACHED_MAY_BE_STALE('tengu_kairos_brief', false))) && isBriefOnly && !viewingAgentTaskId) {
68
68
  return _jsx(BriefSpinner, { mode: props.mode, overrideMessage: props.overrideMessage });
69
69
  }
@@ -73,7 +73,7 @@ function SpinnerWithVerbInner({ mode, loadingStartTimeRef, totalPausedMsRef, pau
73
73
  const settings = useSettings();
74
74
  const reducedMotion = settings.prefersReducedMotion ?? false;
75
75
  // NOTE: useAnimationFrame(50) lives in SpinnerAnimationRow, not here.
76
- // This component only re-renders when props or app state change
76
+ // This component only re-renders when props or app state change —
77
77
  // it is no longer on the 50ms clock. All `time`-derived values
78
78
  // (frame, glimmer, stalled intensity, token counter, thinking shimmer,
79
79
  // elapsed-time timer) are computed inside the child.
@@ -139,7 +139,7 @@ function SpinnerWithVerbInner({ mode, loadingStartTimeRef, totalPausedMsRef, pau
139
139
  // Leader's own verb (always the leader's, regardless of who is foregrounded)
140
140
  const leaderVerb = overrideMessage ?? currentTodo?.activeForm ?? currentTodo?.subject ?? randomVerb;
141
141
  const effectiveVerb = foregroundedTeammate && !foregroundedTeammate.isIdle ? foregroundedTeammate.spinnerVerb ?? randomVerb : leaderVerb;
142
- const message = effectiveVerb + '';
142
+ const message = effectiveVerb + '...';
143
143
  // Track CLI activity when spinner is active
144
144
  useEffect(() => {
145
145
  const operationId = 'spinner-' + mode;
@@ -166,11 +166,11 @@ function SpinnerWithVerbInner({ mode, loadingStartTimeRef, totalPausedMsRef, pau
166
166
  }
167
167
  }
168
168
  }
169
- // Stale read of the refs for showBtwTip below we're off the 50ms clock
169
+ // Stale read of the refs for showBtwTip below — we're off the 50ms clock
170
170
  // so this only updates when props/app state change, which is sufficient for
171
171
  // a coarse 30s threshold.
172
172
  const elapsedSnapshot = pauseStartTimeRef.current !== null ? pauseStartTimeRef.current - loadingStartTimeRef.current - totalPausedMsRef.current : Date.now() - loadingStartTimeRef.current - totalPausedMsRef.current;
173
- // Leader token count for TeammateSpinnerTree read raw (non-animated) from
173
+ // Leader token count for TeammateSpinnerTree — read raw (non-animated) from
174
174
  // the ref. The tree is only shown when teammates are running; teammate
175
175
  // progress updates to s.tasks trigger re-renders that keep this fresh.
176
176
  const leaderTokenCount = Math.round(responseLengthRef.current / 4);
@@ -179,7 +179,7 @@ function SpinnerWithVerbInner({ mode, loadingStartTimeRef, totalPausedMsRef, pau
179
179
  const messageColor = overrideColor ?? defaultColor;
180
180
  const shimmerColor = overrideShimmerColor ?? defaultShimmerColor;
181
181
  // Compute TTFT string here (off the 50ms animation clock) and pass to
182
- // SpinnerAnimationRow so it folds into the `(thought for Ns · ...)` status
182
+ // SpinnerAnimationRow so it folds into the `(thought for Ns · ...)` status
183
183
  // line instead of taking a separate row. apiMetricsRef is a ref so this
184
184
  // doesn't trigger re-renders; we pick up updates on the parent's ~25x/turn
185
185
  // re-render cadence, same as the old ApiMetricsLine did.
@@ -188,14 +188,14 @@ function SpinnerWithVerbInner({ mode, loadingStartTimeRef, totalPausedMsRef, pau
188
188
  ttftText = computeTtftText(apiMetricsRef.current);
189
189
  }
190
190
  // When leader is idle but teammates are running (and we're viewing the leader),
191
- // show a static dim idle display instead of the animated spinner otherwise
191
+ // show a static dim idle display instead of the animated spinner — otherwise
192
192
  // useStalledAnimation detects no new tokens after 3s and turns the spinner red.
193
193
  if (leaderIsIdle && hasRunningTeammates && !foregroundedTeammate) {
194
- return _jsxs(Box, { flexDirection: "column", width: "100%", alignItems: "flex-start", children: [_jsx(Box, { flexDirection: "row", flexWrap: "wrap", marginTop: 1, width: "100%", children: _jsxs(Text, { dimColor: true, children: [TEARDROP_ASTERISK, " Inactivo", !allIdle && ' · compañeros ejecutándose'] }) }), showSpinnerTree && _jsx(TeammateSpinnerTree, { selectedIndex: selectedIPAgentIndex, isInSelectionMode: viewSelectionMode === 'selecting-agent', allIdle: allIdle, leaderTokenCount: leaderTokenCount, leaderIdleText: "Inactivo" })] });
194
+ return _jsxs(Box, { flexDirection: "column", width: "100%", alignItems: "flex-start", children: [_jsx(Box, { flexDirection: "row", flexWrap: "wrap", marginTop: 1, width: "100%", children: _jsxs(Text, { dimColor: true, children: [TEARDROP_ASTERISK, " Inactivo", !allIdle && ' · companeros ejecutandose'] }) }), showSpinnerTree && _jsx(TeammateSpinnerTree, { selectedIndex: selectedIPAgentIndex, isInSelectionMode: viewSelectionMode === 'selecting-agent', allIdle: allIdle, leaderTokenCount: leaderTokenCount, leaderIdleText: "Inactivo" })] });
195
195
  }
196
196
  // When viewing an idle teammate, show static idle display instead of animated spinner
197
197
  if (foregroundedTeammate?.isIdle) {
198
- const idleText = allIdle ? `${TEARDROP_ASTERISK} Trabajó durante ${formatDuration(Date.now() - foregroundedTeammate.startTime)}` : `${TEARDROP_ASTERISK} Inactivo`;
198
+ const idleText = allIdle ? `${TEARDROP_ASTERISK} Trabajo durante ${formatDuration(Date.now() - foregroundedTeammate.startTime)}` : `${TEARDROP_ASTERISK} Inactivo`;
199
199
  return _jsxs(Box, { flexDirection: "column", width: "100%", alignItems: "flex-start", children: [_jsx(Box, { flexDirection: "row", flexWrap: "wrap", marginTop: 1, width: "100%", children: _jsx(Text, { dimColor: true, children: idleText }) }), showSpinnerTree && hasRunningTeammates && _jsx(TeammateSpinnerTree, { selectedIndex: selectedIPAgentIndex, isInSelectionMode: viewSelectionMode === 'selecting-agent', allIdle: allIdle, leaderVerb: leaderIsIdle ? undefined : leaderVerb, leaderIdleText: leaderIsIdle ? 'Inactivo' : undefined, leaderTokenCount: leaderTokenCount })] });
200
200
  }
201
201
  // Time-based tip overrides: coarse thresholds so a stale ref read (we're
@@ -205,8 +205,8 @@ function SpinnerWithVerbInner({ mode, loadingStartTimeRef, totalPausedMsRef, pau
205
205
  const tipsEnabled = settings.spinnerTipsEnabled !== false;
206
206
  const showClearTip = tipsEnabled && elapsedSnapshot > 1_800_000;
207
207
  const showBtwTip = tipsEnabled && elapsedSnapshot > 30_000 && !getGlobalConfig().btwUseCount;
208
- const effectiveTip = contextTipsActive ? undefined : showClearTip && !nextTask ? 'Usa /clear para empezar de cero al cambiar de tema y liberar contexto' : showBtwTip && !nextTask ? "Usa /btw para hacer una pregunta rápida sin interrumpir el trabajo actual de Context" : spinnerTip;
209
- // Budget text (ant-only) shown above the tip line
208
+ const effectiveTip = contextTipsActive ? undefined : showClearTip && !nextTask ? 'Usa /clear para empezar de cero al cambiar de tema y liberar contexto' : showBtwTip && !nextTask ? "Usa /btw para hacer una pregunta rapida sin interrumpir el trabajo actual de Context" : spinnerTip;
209
+ // Budget text (ant-only) — shown above the tip line
210
210
  let budgetText = null;
211
211
  if (feature('TOKEN_BUDGET')) {
212
212
  const budget = getCurrentTurnTokenBudget();
@@ -428,7 +428,7 @@ export function Spinner() {
428
428
  if (reducedMotion) {
429
429
  let t0;
430
430
  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
431
- t0 = _jsx(Text, { color: "text", children: "\u25CF" });
431
+ t0 = _jsx(Text, { color: "text", children: "*" });
432
432
  $[0] = t0;
433
433
  }
434
434
  else {
@@ -58,7 +58,7 @@ export const SPINNER_VERBS = [
58
58
  'Descifrando',
59
59
  'Deliberando',
60
60
  'Determinando',
61
- 'Entreteniéndose',
61
+ 'Entreteniendose',
62
62
  'Desconcertando',
63
63
  'Haciendo',
64
64
  'Garabateando',
@@ -137,7 +137,7 @@ export const SPINNER_VERBS = [
137
137
  'Polinizando',
138
138
  'Ponderando',
139
139
  'Pontificando',
140
- 'Avalanzándose',
140
+ 'Avalanzandose',
141
141
  'Precipitando',
142
142
  'Haciendo magia',
143
143
  'Procesando',
@@ -155,17 +155,17 @@ export const SPINNER_VERBS = [
155
155
  'Salteando',
156
156
  'Correteando',
157
157
  'Cargando',
158
- 'Escabulléndose',
158
+ 'Escabullendose',
159
159
  'Sazonando',
160
160
  'Traveseando',
161
- 'Meneándose',
161
+ 'Meneandose',
162
162
  'Cociendo a fuego lento',
163
163
  'Escapando',
164
164
  'Bocetando',
165
- 'Deslizándose',
165
+ 'Deslizandose',
166
166
  'Aplastando',
167
167
  'Bailando en calcetines',
168
- 'Espeleología',
168
+ 'Espeleologia',
169
169
  'Girando',
170
170
  'Brotando',
171
171
  'Estofando',
@@ -178,8 +178,8 @@ export const SPINNER_VERBS = [
178
178
  'Pensando',
179
179
  'Tronando',
180
180
  'Trasteando',
181
- 'Haciendo tonterías',
182
- 'Poniendo del revés',
181
+ 'Haciendo tonterias',
182
+ 'Poniendo del reves',
183
183
  'Transfigurando',
184
184
  'Transmutando',
185
185
  'Retorciendo',
@@ -187,7 +187,7 @@ export const SPINNER_VERBS = [
187
187
  'Desplegando',
188
188
  'Desenredando',
189
189
  'Vibrando',
190
- 'Contoneándose',
190
+ 'Contoneandose',
191
191
  'Vagando',
192
192
  'Deformando',
193
193
  'Chirimboleando',
@@ -118,9 +118,10 @@ export function usePasteHandler({ onPaste, onInput, onImagePaste, }) {
118
118
  });
119
119
  return { chunks: [], timeoutId: null };
120
120
  }
121
- // If paste is empty (common when trying to paste images with Cmd+V),
122
- // check if clipboard has an image (macOS only)
123
- if (isMacOS && onImagePaste && pastedText.length === 0) {
121
+ // If paste is empty, try resolving it as an image from the clipboard.
122
+ // Some terminals/platforms deliver image paste as an empty bracketed
123
+ // paste instead of text content.
124
+ if (onImagePaste && pastedText.length === 0) {
124
125
  checkClipboardForImage();
125
126
  return { chunks: [], timeoutId: null };
126
127
  }
@@ -162,11 +163,10 @@ export function usePasteHandler({ onPaste, onInput, onImagePaste, }) {
162
163
  .split(/ (?=\/|[A-Za-z]:\\)/)
163
164
  .flatMap(part => part.split('\n'))
164
165
  .some(line => isImageFilePath(line.trim()));
165
- // Handle empty paste (clipboard image on macOS)
166
- // When the user pastes an image with Cmd+V, the terminal sends an empty
167
- // bracketed paste sequence. The keypress parser emits this as isPasted=true
168
- // with empty input.
169
- if (isFromPaste && input.length === 0 && isMacOS && onImagePaste) {
166
+ // Handle empty paste as a potential clipboard image.
167
+ // Some terminals emit an empty bracketed paste sequence when the clipboard
168
+ // contains an image instead of text.
169
+ if (isFromPaste && input.length === 0 && onImagePaste) {
170
170
  checkClipboardForImage();
171
171
  // Reset isPasting since there's no text content to process
172
172
  setIsPasting(false);