Compare commits
1 Commits
fix/issue-
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9906b33fa0 |
@@ -111,7 +111,7 @@ import { BackgroundTasksDialog } from '../tasks/BackgroundTasksDialog.js';
|
||||
import { shouldHideTasksFooter } from '../tasks/taskStatusUtils.js';
|
||||
import { TeamsDialog } from '../teams/TeamsDialog.js';
|
||||
import VimTextInput from '../VimTextInput.js';
|
||||
import { detectModeEntry, getModeFromInput, getValueFromInput } from './inputModes.js';
|
||||
import { getModeFromInput, getValueFromInput } from './inputModes.js';
|
||||
import { FOOTER_TEMPORARY_STATUS_TIMEOUT, Notifications } from './Notifications.js';
|
||||
import PromptInputFooter from './PromptInputFooter.js';
|
||||
import type { SuggestionItem } from './PromptInputFooterSuggestions.js';
|
||||
@@ -878,22 +878,24 @@ function PromptInput({
|
||||
abortPromptSuggestion();
|
||||
abortSpeculation(setAppState);
|
||||
|
||||
// Strip the mode character from the buffer when entering bash mode — the
|
||||
// mode itself is shown via the prompt prefix in the UI. Without this,
|
||||
// typing `!` into empty input would enter bash mode but leave the literal
|
||||
// `!` in the buffer (issue #662).
|
||||
const modeEntry = detectModeEntry({
|
||||
value,
|
||||
prevInputLength: input.length,
|
||||
cursorOffset,
|
||||
});
|
||||
if (modeEntry) {
|
||||
onModeChange(modeEntry.mode);
|
||||
const cleaned = modeEntry.strippedValue.replaceAll('\t', ' ');
|
||||
pushToBuffer(input, cursorOffset, pastedContents);
|
||||
trackAndSetInput(cleaned);
|
||||
setCursorOffset(cleaned.length);
|
||||
return;
|
||||
// Check if this is a single character insertion at the start
|
||||
const isSingleCharInsertion = value.length === input.length + 1;
|
||||
const insertedAtStart = cursorOffset === 0;
|
||||
const mode = getModeFromInput(value);
|
||||
if (insertedAtStart && mode !== 'prompt') {
|
||||
if (isSingleCharInsertion) {
|
||||
onModeChange(mode);
|
||||
return;
|
||||
}
|
||||
// Multi-char insertion into empty input (e.g. tab-accepting "! gcloud auth login")
|
||||
if (input.length === 0) {
|
||||
onModeChange(mode);
|
||||
const valueWithoutMode = getValueFromInput(value).replaceAll('\t', ' ');
|
||||
pushToBuffer(input, cursorOffset, pastedContents);
|
||||
trackAndSetInput(valueWithoutMode);
|
||||
setCursorOffset(valueWithoutMode.length);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const processedValue = value.replaceAll('\t', ' ');
|
||||
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import { describe, expect, it } from 'bun:test'
|
||||
import {
|
||||
detectModeEntry,
|
||||
getModeFromInput,
|
||||
getValueFromInput,
|
||||
isInputModeCharacter,
|
||||
prependModeCharacterToInput,
|
||||
} from './inputModes.js'
|
||||
|
||||
describe('inputModes', () => {
|
||||
describe('getModeFromInput', () => {
|
||||
it('returns bash mode for input starting with !', () => {
|
||||
expect(getModeFromInput('!')).toBe('bash')
|
||||
expect(getModeFromInput('!ls')).toBe('bash')
|
||||
})
|
||||
|
||||
it('returns prompt mode for non-bash input', () => {
|
||||
expect(getModeFromInput('')).toBe('prompt')
|
||||
expect(getModeFromInput('hello')).toBe('prompt')
|
||||
expect(getModeFromInput(' !')).toBe('prompt')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getValueFromInput', () => {
|
||||
it('strips the leading ! when entering bash mode', () => {
|
||||
expect(getValueFromInput('!')).toBe('')
|
||||
expect(getValueFromInput('!ls -la')).toBe('ls -la')
|
||||
})
|
||||
|
||||
it('returns input unchanged in prompt mode', () => {
|
||||
expect(getValueFromInput('')).toBe('')
|
||||
expect(getValueFromInput('hello')).toBe('hello')
|
||||
})
|
||||
})
|
||||
|
||||
describe('isInputModeCharacter', () => {
|
||||
it('returns true only for the bare ! character', () => {
|
||||
expect(isInputModeCharacter('!')).toBe(true)
|
||||
expect(isInputModeCharacter('!ls')).toBe(false)
|
||||
expect(isInputModeCharacter('')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('prependModeCharacterToInput', () => {
|
||||
it('prepends ! when mode is bash', () => {
|
||||
expect(prependModeCharacterToInput('ls', 'bash')).toBe('!ls')
|
||||
expect(prependModeCharacterToInput('', 'bash')).toBe('!')
|
||||
})
|
||||
|
||||
it('returns input unchanged in prompt mode', () => {
|
||||
expect(prependModeCharacterToInput('hello', 'prompt')).toBe('hello')
|
||||
})
|
||||
})
|
||||
|
||||
describe('detectModeEntry', () => {
|
||||
// Regression for #662 — typing `!` into empty input must switch to bash
|
||||
// mode AND yield an empty stripped buffer. Before the fix the single-char
|
||||
// path returned without stripping, leaving `!` visible in the buffer.
|
||||
it('strips the mode character when typing ! into empty input', () => {
|
||||
expect(
|
||||
detectModeEntry({ value: '!', prevInputLength: 0, cursorOffset: 0 }),
|
||||
).toEqual({ mode: 'bash', strippedValue: '' })
|
||||
})
|
||||
|
||||
it('strips the mode character when pasting !cmd into empty input', () => {
|
||||
expect(
|
||||
detectModeEntry({ value: '!ls -la', prevInputLength: 0, cursorOffset: 0 }),
|
||||
).toEqual({ mode: 'bash', strippedValue: 'ls -la' })
|
||||
})
|
||||
|
||||
it('returns null when the cursor is not at the start', () => {
|
||||
expect(
|
||||
detectModeEntry({ value: '!', prevInputLength: 0, cursorOffset: 1 }),
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null when the value does not start with !', () => {
|
||||
expect(
|
||||
detectModeEntry({ value: 'hello', prevInputLength: 0, cursorOffset: 0 }),
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null when typing ! after existing text', () => {
|
||||
// value="ab!" with prevInputLength=2 is a single-char insertion but does
|
||||
// not start with ! — getModeFromInput returns 'prompt'.
|
||||
expect(
|
||||
detectModeEntry({ value: 'ab!', prevInputLength: 2, cursorOffset: 0 }),
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null when prepending ! to non-empty existing text', () => {
|
||||
// Single-char insertion at start that produces "!ab" from "ab" — value
|
||||
// length is 3, prevInputLength is 2, so isSingleCharInsertion is true
|
||||
// and isMultiCharIntoEmpty is false. We accept the mode change here so
|
||||
// that typing ! at the start of existing text still toggles mode.
|
||||
const result = detectModeEntry({
|
||||
value: '!ab',
|
||||
prevInputLength: 2,
|
||||
cursorOffset: 0,
|
||||
})
|
||||
expect(result).toEqual({ mode: 'bash', strippedValue: 'ab' })
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -31,30 +31,3 @@ export function getValueFromInput(input: string): string {
|
||||
export function isInputModeCharacter(input: string): boolean {
|
||||
return input === '!'
|
||||
}
|
||||
|
||||
export type ModeEntryDecision = {
|
||||
mode: HistoryMode
|
||||
strippedValue: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide whether an onChange `value` should switch the input mode (e.g.
|
||||
* `prompt` → `bash`) and what the stripped buffer value should be.
|
||||
*
|
||||
* Returns null when no mode change applies. Returns a decision otherwise so
|
||||
* callers run a single update path — no separate single-char vs multi-char
|
||||
* branches that can drift apart.
|
||||
*/
|
||||
export function detectModeEntry(args: {
|
||||
value: string
|
||||
prevInputLength: number
|
||||
cursorOffset: number
|
||||
}): ModeEntryDecision | null {
|
||||
if (args.cursorOffset !== 0) return null
|
||||
const mode = getModeFromInput(args.value)
|
||||
if (mode === 'prompt') return null
|
||||
const isSingleCharInsertion = args.value.length === args.prevInputLength + 1
|
||||
const isMultiCharIntoEmpty = args.prevInputLength === 0
|
||||
if (!isSingleCharInsertion && !isMultiCharIntoEmpty) return null
|
||||
return { mode, strippedValue: getValueFromInput(args.value) }
|
||||
}
|
||||
|
||||
7
src/constants/querySource.ts
Normal file
7
src/constants/querySource.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Stub — query source enum not included in source snapshot. See
|
||||
* src/types/message.ts for the same scoping caveat (issue #473).
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export type QuerySource = any
|
||||
@@ -442,7 +442,84 @@ export async function connectRemoteControl(
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
|
||||
// add exit reason types for removing the error within gracefulShutdown file
|
||||
// add exit reason types for removing the error within gracefulShutdown file
|
||||
export type ExitReason = {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Stub re-exports — types not included in source snapshot.
|
||||
//
|
||||
// The upstream Anthropic SDK defines these in sub-files (sdk/coreTypes,
|
||||
// sdk/runtimeTypes, sdk/controlTypes, sdk/toolTypes) that are stubbed
|
||||
// in this open repo. Until the real definitions are restored, alias the
|
||||
// names to `any` so callers can resolve their imports and `tsc` becomes
|
||||
// actionable. See issue #473 for the typecheck-foundation effort.
|
||||
// ============================================================================
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export type AnyZodRawShape = any
|
||||
export type ApiKeySource = any
|
||||
export type AsyncHookJSONOutput = any
|
||||
export type ConfigChangeHookInput = any
|
||||
export type CwdChangedHookInput = any
|
||||
export type ElicitationHookInput = any
|
||||
export type ElicitationResultHookInput = any
|
||||
export type FileChangedHookInput = any
|
||||
export type ForkSessionOptions = any
|
||||
export type ForkSessionResult = any
|
||||
export type GetSessionInfoOptions = any
|
||||
export type GetSessionMessagesOptions = any
|
||||
export type HookEvent = any
|
||||
export type HookInput = any
|
||||
export type HookJSONOutput = any
|
||||
export type InferShape<_T> = any
|
||||
export type InstructionsLoadedHookInput = any
|
||||
export type InternalOptions = any
|
||||
export type InternalQuery = any
|
||||
export type ListSessionsOptions = any
|
||||
export type McpSdkServerConfigWithInstance = any
|
||||
export type McpServerConfigForProcessTransport = any
|
||||
export type McpServerStatus = any
|
||||
export type ModelInfo = any
|
||||
export type ModelUsage = any
|
||||
export type NotificationHookInput = any
|
||||
export type Options = any
|
||||
export type PermissionDeniedHookInput = any
|
||||
export type PermissionMode = any
|
||||
export type PermissionRequestHookInput = any
|
||||
export type PermissionResult = any
|
||||
export type PermissionUpdate = any
|
||||
export type PostCompactHookInput = any
|
||||
export type PostToolUseFailureHookInput = any
|
||||
export type PostToolUseHookInput = any
|
||||
export type PreCompactHookInput = any
|
||||
export type PreToolUseHookInput = any
|
||||
export type Query = any
|
||||
export type RewindFilesResult = any
|
||||
export type SDKAssistantMessage = any
|
||||
export type SDKAssistantMessageError = any
|
||||
export type SDKCompactBoundaryMessage = any
|
||||
export type SdkMcpToolDefinition = any
|
||||
export type SDKPartialAssistantMessage = any
|
||||
export type SDKPermissionDenial = any
|
||||
export type SDKRateLimitInfo = any
|
||||
export type SDKStatus = any
|
||||
export type SDKStatusMessage = any
|
||||
export type SDKSystemMessage = any
|
||||
export type SDKToolProgressMessage = any
|
||||
export type SDKUserMessageReplay = any
|
||||
export type SessionEndHookInput = any
|
||||
export type SessionMessage = any
|
||||
export type SessionMutationOptions = any
|
||||
export type SessionStartHookInput = any
|
||||
export type SetupHookInput = any
|
||||
export type StopFailureHookInput = any
|
||||
export type StopHookInput = any
|
||||
export type SubagentStartHookInput = any
|
||||
export type SubagentStopHookInput = any
|
||||
export type SyncHookJSONOutput = any
|
||||
export type TaskCompletedHookInput = any
|
||||
export type TaskCreatedHookInput = any
|
||||
export type TeammateIdleHookInput = any
|
||||
export type UserPromptSubmitHookInput = any
|
||||
10
src/entrypoints/sdk/controlTypes.ts
Normal file
10
src/entrypoints/sdk/controlTypes.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Stub — control protocol types not included in source snapshot. See
|
||||
* src/types/message.ts for the same scoping caveat (issue #473).
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export type SDKControlRequest = any
|
||||
export type SDKControlResponse = any
|
||||
export type SDKControlPermissionRequest = any
|
||||
export type StdoutMessage = any
|
||||
16
src/global.d.ts
vendored
Normal file
16
src/global.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Build-time globals replaced by the bundler at build time.
|
||||
*
|
||||
* `scripts/build.ts` substitutes these via Bun's `define` option, so at
|
||||
* runtime the references are inlined as string literals. This declaration
|
||||
* exists only to make `tsc --noEmit` aware of them — without it, every
|
||||
* `MACRO.*` access fires TS2304 "Cannot find name 'MACRO'".
|
||||
*/
|
||||
declare const MACRO: {
|
||||
VERSION: string
|
||||
DISPLAY_VERSION: string
|
||||
BUILD_TIME: string
|
||||
ISSUES_EXPLAINER: string
|
||||
PACKAGE_URL: string
|
||||
NATIVE_PACKAGE_URL: string | undefined
|
||||
}
|
||||
25
src/types/message.ts
Normal file
25
src/types/message.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Stub — message type definitions not included in source snapshot.
|
||||
*
|
||||
* The upstream Anthropic source defines a rich Message discriminated union
|
||||
* with structured Content blocks, role tags, tool_use payloads, and so on.
|
||||
* That file is not mirrored to this open snapshot. This stub exists so
|
||||
* `tsc --noEmit` can resolve `import { Message, ... } from 'src/types/message'`
|
||||
* across the ~21 callers without fixing every transitive type the call
|
||||
* sites use.
|
||||
*
|
||||
* Once the real definitions are restored upstream-side or reconstructed
|
||||
* from runtime usage, replace these `any` aliases with proper types and
|
||||
* delete this comment. See issue #473 for the typecheck-foundation effort.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export type Message = any
|
||||
export type AssistantMessage = any
|
||||
export type UserMessage = any
|
||||
export type SystemMessage = any
|
||||
export type SystemAPIErrorMessage = any
|
||||
export type AttachmentMessage = any
|
||||
export type ProgressMessage = any
|
||||
export type HookResultMessage = any
|
||||
export type NormalizedUserMessage = any
|
||||
7
src/types/tools.ts
Normal file
7
src/types/tools.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Stub — tool type definitions not included in source snapshot. See
|
||||
* src/types/message.ts for the same scoping caveat (issue #473).
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export type ShellProgress = any
|
||||
15
src/types/utils.ts
Normal file
15
src/types/utils.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Stub — utility type definitions not included in source snapshot. See
|
||||
* src/types/message.ts for the same scoping caveat (issue #473).
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export type DeepImmutable<T> = T extends any[]
|
||||
? readonly DeepImmutable<T[number]>[]
|
||||
: T extends object
|
||||
? { readonly [K in keyof T]: DeepImmutable<T[K]> }
|
||||
: T
|
||||
|
||||
export type Permutations<T extends string, U extends string = T> = T extends T
|
||||
? T | `${T}${Permutations<Exclude<U, T>>}`
|
||||
: never
|
||||
@@ -1,10 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023", "DOM"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"noEmit": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
Reference in New Issue
Block a user