diff --git a/src/buddy/companionReact.ts b/src/buddy/companionReact.ts index 021167e0d..f673004f2 100644 --- a/src/buddy/companionReact.ts +++ b/src/buddy/companionReact.ts @@ -73,7 +73,7 @@ function isAddressed(messages: Message[], name: string): boolean { ) { const m = messages[i] if (m?.type !== 'user') continue - const content = (m as any).message?.content + const content = m.message?.content if (typeof content === 'string' && pattern.test(content)) return true } return false @@ -89,7 +89,7 @@ function buildTranscript(messages: Message[]): string { .filter(m => m.type === 'user' || m.type === 'assistant') .map(m => { const role = m.type === 'user' ? 'user' : 'claude' - const content = (m as any).message?.content + const content = m.message?.content const text = typeof content === 'string' ? content.slice(0, 300) diff --git a/src/cli/transports/ccrClient.ts b/src/cli/transports/ccrClient.ts index e1d5f6f11..09c7c05ef 100644 --- a/src/cli/transports/ccrClient.ts +++ b/src/cli/transports/ccrClient.ts @@ -381,7 +381,7 @@ export class CCRClient { if (!result.ok) { throw new RetryableError( 'client event POST failed', - (result as any).retryAfterMs, + result.retryAfterMs, ) } }, @@ -404,7 +404,7 @@ export class CCRClient { if (!result.ok) { throw new RetryableError( 'internal event POST failed', - (result as any).retryAfterMs, + result.retryAfterMs, ) } }, @@ -433,10 +433,7 @@ export class CCRClient { 'delivery batch', ) if (!result.ok) { - throw new RetryableError( - 'delivery POST failed', - (result as any).retryAfterMs, - ) + throw new RetryableError('delivery POST failed', result.retryAfterMs) } }, baseDelayMs: 500, diff --git a/src/components/ConsoleOAuthFlow.tsx b/src/components/ConsoleOAuthFlow.tsx index 084cdf2d0..f727ca5f2 100644 --- a/src/components/ConsoleOAuthFlow.tsx +++ b/src/components/ConsoleOAuthFlow.tsx @@ -272,7 +272,9 @@ export function ConsoleOAuthFlow({ throw new Error((orgResult as { valid: false; message: string }).message); } // Reset modelType to anthropic when using OAuth login - updateSettingsForSource('userSettings', { modelType: 'anthropic' } as any); + updateSettingsForSource('userSettings', { modelType: 'anthropic' } as unknown as Parameters< + typeof updateSettingsForSource + >[1]); setOAuthStatus({ state: 'success' }); void sendNotification( @@ -662,9 +664,9 @@ function OAuthStatusMessage({ if (finalVals.sonnet_model) env.ANTHROPIC_DEFAULT_SONNET_MODEL = finalVals.sonnet_model; if (finalVals.opus_model) env.ANTHROPIC_DEFAULT_OPUS_MODEL = finalVals.opus_model; const { error } = updateSettingsForSource('userSettings', { - modelType: 'anthropic' as any, + modelType: 'anthropic', env, - } as any); + } as unknown as Parameters[1]); if (error) { setOAuthStatus({ state: 'error', @@ -1153,9 +1155,9 @@ function OAuthStatusMessage({ if (finalVals.sonnet_model) env.GEMINI_DEFAULT_SONNET_MODEL = finalVals.sonnet_model; if (finalVals.opus_model) env.GEMINI_DEFAULT_OPUS_MODEL = finalVals.opus_model; const { error } = updateSettingsForSource('userSettings', { - modelType: 'gemini' as any, + modelType: 'gemini', env, - } as any); + } as unknown as Parameters[1]); if (error) { setOAuthStatus({ state: 'error', diff --git a/src/components/FeedbackSurvey/useFrustrationDetection.ts b/src/components/FeedbackSurvey/useFrustrationDetection.ts index d88eb2d56..2d04e9f03 100644 --- a/src/components/FeedbackSurvey/useFrustrationDetection.ts +++ b/src/components/FeedbackSurvey/useFrustrationDetection.ts @@ -12,7 +12,9 @@ export type FrustrationDetectionResult = { } function detectFrustration(messages: Message[]): boolean { - const apiErrors = messages.filter(m => (m as any).isApiErrorMessage) + const apiErrors = messages.filter( + m => 'isApiErrorMessage' in m && m.isApiErrorMessage === true, + ) return apiErrors.length >= 2 } @@ -25,7 +27,9 @@ export function useFrustrationDetection( const [state, setState] = useState('closed') const config = getGlobalConfig() as { transcriptShareDismissed?: boolean } - const policyAllowed = isPolicyAllowed('product_feedback' as any) + const policyAllowed = isPolicyAllowed( + 'product_feedback' as Parameters[0], + ) const shouldSkip = config.transcriptShareDismissed || !policyAllowed || diff --git a/src/components/PromptInput/PromptInputFooterLeftSide.tsx b/src/components/PromptInput/PromptInputFooterLeftSide.tsx index efa289772..4ac476bf2 100644 --- a/src/components/PromptInput/PromptInputFooterLeftSide.tsx +++ b/src/components/PromptInput/PromptInputFooterLeftSide.tsx @@ -55,6 +55,7 @@ const NULL = () => null; const MAX_VOICE_HINT_SHOWS = 3; const RSS_UPDATE_INTERVAL_MS = 5_000; +const GOAL_TICK_INTERVAL_MS = 1_000; type RssState = { text: string; level: 'normal' | 'warning' | 'error' }; @@ -127,6 +128,56 @@ function ProactiveCountdown(): React.ReactNode { return waiting {formatDuration(remainingSeconds * 1000, { mostSignificantOnly: true })}; } +/** Compact "goal (1h22min)" pill for the footer — colored by status. */ +function GoalElapsedIndicator(): React.ReactNode { + const [tick, setTick] = useState(0); + useEffect(() => { + const id = setInterval(() => setTick(t => t + 1), GOAL_TICK_INTERVAL_MS); + return () => clearInterval(id); + }, []); + void tick; + + const { getGoal, getActiveElapsedMs } = + require('../../services/goal/goalState.js') as typeof import('../../services/goal/goalState.js'); + const goal = getGoal(); + if (!goal) return null; + + const elapsedMs = getActiveElapsedMs(goal); + const totalSeconds = Math.floor(elapsedMs / 1000); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + let timeStr: string; + if (hours >= 1) { + timeStr = `${hours}h${minutes}min`; + } else if (minutes >= 1) { + timeStr = `${minutes}min`; + } else { + timeStr = `${seconds}s`; + } + + let color: string | undefined; + switch (goal.status) { + case 'active': + color = 'ansi:green'; + break; + case 'paused': + case 'budget_limited': + case 'usage_limited': + color = 'ansi:yellow'; + break; + case 'blocked': + color = 'ansi:red'; + break; + case 'complete': + color = 'ansi:cyan'; + break; + } + + return goal ({timeStr}); +} + export function PromptInputFooterLeftSide({ exitMessage, vimMode, @@ -376,6 +427,11 @@ function ModeIndicator({ , ] : []), + // Goal elapsed indicator — compact "goal (XhYmin)" after PID + ...(feature('GOAL') && + (require('../../services/goal/goalState.js') as typeof import('../../services/goal/goalState.js')).getGoal() + ? [] + : []), ]; // Check if any in-process teammates exist (for hint text cycling) diff --git a/src/entrypoints/mcp.ts b/src/entrypoints/mcp.ts index cbe36d5be..c0f2407e7 100644 --- a/src/entrypoints/mcp.ts +++ b/src/entrypoints/mcp.ts @@ -144,7 +144,7 @@ export async function startMCPServer( ) if (validationResult && !validationResult.result) { throw new Error( - `Tool ${name} input is invalid: ${(validationResult as any).message}`, + `Tool ${name} input is invalid: ${'message' in validationResult ? validationResult.message : String(validationResult)}`, ) } const finalResult = await tool.call( diff --git a/src/services/api/gemini/index.ts b/src/services/api/gemini/index.ts index 6754189af..accb2fb0b 100644 --- a/src/services/api/gemini/index.ts +++ b/src/services/api/gemini/index.ts @@ -1,4 +1,7 @@ -import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' +import type { + BetaToolUnion, + BetaMessage, +} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' import { randomUUID } from 'crypto' import type { AssistantMessage, @@ -112,21 +115,21 @@ export async function* queryModelGemini( ) const adaptedStream = adaptGeminiStreamToAnthropic(stream, geminiModel) - const contentBlocks: Record = {} + const contentBlocks: Record> = {} const collectedMessages: AssistantMessage[] = [] - let partialMessage: any + let partialMessage: BetaMessage | null = null let ttftMs = 0 const start = Date.now() for await (const event of adaptedStream) { switch (event.type) { case 'message_start': - partialMessage = (event as any).message + partialMessage = event.message ttftMs = Date.now() - start break case 'content_block_start': { - const idx = (event as any).index - const cb = (event as any).content_block + const idx = event.index + const cb = event.content_block if (cb.type === 'tool_use') { contentBlocks[idx] = { ...cb, input: '' } } else if (cb.type === 'text') { @@ -139,17 +142,19 @@ export async function* queryModelGemini( break } case 'content_block_delta': { - const idx = (event as any).index - const delta = (event as any).delta + const idx = event.index + const delta = event.delta const block = contentBlocks[idx] if (!block) break if (delta.type === 'text_delta') { - block.text = (block.text || '') + delta.text + block.text = ((block.text as string | undefined) || '') + delta.text } else if (delta.type === 'input_json_delta') { - block.input = (block.input || '') + delta.partial_json + block.input = + ((block.input as string | undefined) || '') + delta.partial_json } else if (delta.type === 'thinking_delta') { - block.thinking = (block.thinking || '') + delta.thinking + block.thinking = + ((block.thinking as string | undefined) || '') + delta.thinking } else if (delta.type === 'signature_delta') { if (block.type === 'thinking') { block.signature = delta.signature @@ -160,15 +165,19 @@ export async function* queryModelGemini( break } case 'content_block_stop': { - const idx = (event as any).index + const idx = event.index const block = contentBlocks[idx] if (!block || !partialMessage) break const message: AssistantMessage = { message: { ...partialMessage, - content: normalizeContentFromAPI([block], tools, options.agentId), - }, + content: normalizeContentFromAPI( + [block] as unknown as BetaMessage['content'], + tools, + options.agentId, + ), + } as AssistantMessage['message'], requestId: undefined, type: 'assistant', uuid: randomUUID(), diff --git a/src/services/api/grok/index.ts b/src/services/api/grok/index.ts index 55c91dad4..cf46de2e0 100644 --- a/src/services/api/grok/index.ts +++ b/src/services/api/grok/index.ts @@ -1,4 +1,8 @@ -import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' +import type { + BetaToolUnion, + BetaMessage, + BetaUsage, +} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' import type { SystemPrompt } from '../../../utils/systemPromptType.js' import type { Message, @@ -119,10 +123,15 @@ export async function* queryModelGrok( grokModel, ) - const contentBlocks: Record = {} + const contentBlocks: Record> = {} const collectedMessages: AssistantMessage[] = [] - let partialMessage: any - let usage = { + let partialMessage: BetaMessage | null = null + let usage: { + input_tokens: number + output_tokens: number + cache_creation_input_tokens: number + cache_read_input_tokens: number + } = { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, @@ -134,16 +143,21 @@ export async function* queryModelGrok( for await (const event of adaptedStream) { switch (event.type) { case 'message_start': { - partialMessage = (event as any).message + partialMessage = event.message ttftMs = Date.now() - start - if ((event as any).message?.usage) { - usage = updateOpenAIUsage(usage, (event as any).message.usage) + if (event.message.usage) { + usage = updateOpenAIUsage( + usage, + event.message.usage as unknown as Parameters< + typeof updateOpenAIUsage + >[1], + ) } break } case 'content_block_start': { - const idx = (event as any).index - const cb = (event as any).content_block + const idx = event.index + const cb = event.content_block if (cb.type === 'tool_use') { contentBlocks[idx] = { ...cb, input: '' } } else if (cb.type === 'text') { @@ -156,31 +170,37 @@ export async function* queryModelGrok( break } case 'content_block_delta': { - const idx = (event as any).index - const delta = (event as any).delta + const idx = event.index + const delta = event.delta const block = contentBlocks[idx] if (!block) break if (delta.type === 'text_delta') { - block.text = (block.text || '') + delta.text + block.text = ((block.text as string | undefined) || '') + delta.text } else if (delta.type === 'input_json_delta') { - block.input = (block.input || '') + delta.partial_json + block.input = + ((block.input as string | undefined) || '') + delta.partial_json } else if (delta.type === 'thinking_delta') { - block.thinking = (block.thinking || '') + delta.thinking + block.thinking = + ((block.thinking as string | undefined) || '') + delta.thinking } else if (delta.type === 'signature_delta') { block.signature = delta.signature } break } case 'content_block_stop': { - const idx = (event as any).index + const idx = event.index const block = contentBlocks[idx] if (!block || !partialMessage) break const m: AssistantMessage = { message: { ...partialMessage, - content: normalizeContentFromAPI([block], tools, options.agentId), - }, + content: normalizeContentFromAPI( + [block] as unknown as BetaMessage['content'], + tools, + options.agentId, + ), + } as AssistantMessage['message'], requestId: undefined, type: 'assistant', uuid: randomUUID(), @@ -191,9 +211,12 @@ export async function* queryModelGrok( break } case 'message_delta': { - const deltaUsage = (event as any).usage + const deltaUsage = event.usage if (deltaUsage) { - usage = updateOpenAIUsage(usage, deltaUsage) + usage = updateOpenAIUsage( + usage, + deltaUsage as unknown as Parameters[1], + ) } break } @@ -205,8 +228,15 @@ export async function* queryModelGrok( event.type === 'message_stop' && usage.input_tokens + usage.output_tokens > 0 ) { - const costUSD = calculateUSDCost(grokModel, usage as any) - addToTotalSessionCost(costUSD, usage as any, options.model) + const costUSD = calculateUSDCost( + grokModel, + usage as unknown as BetaUsage, + ) + addToTotalSessionCost( + costUSD, + usage as unknown as BetaUsage, + options.model, + ) } yield { diff --git a/src/services/api/openai/index.ts b/src/services/api/openai/index.ts index b76be9b9b..f165d4026 100644 --- a/src/services/api/openai/index.ts +++ b/src/services/api/openai/index.ts @@ -1,4 +1,8 @@ -import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' +import type { + BetaToolUnion, + BetaMessage, + BetaUsage, +} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' import type { SystemPrompt } from '../../../utils/systemPromptType.js' import type { Message, @@ -137,8 +141,8 @@ function isOpenAIConvertibleMessage( * `message_stop` handler and the post-loop safety fallback. */ function assembleFinalAssistantOutputs(params: { - partialMessage: any - contentBlocks: Record + partialMessage: BetaMessage | null + contentBlocks: Record> tools: Tools agentId: string | undefined usage: { @@ -166,19 +170,19 @@ function assembleFinalAssistantOutputs(params: { .map(k => contentBlocks[Number(k)]) .filter(Boolean) - if (allBlocks.length > 0) { + if (allBlocks.length > 0 && partialMessage) { outputs.push({ message: { ...partialMessage, content: normalizeContentFromAPI( - allBlocks, + allBlocks as unknown as BetaMessage['content'], tools, agentId as AgentId | undefined, ), usage, stop_reason: stopReason, stop_sequence: null, - }, + } as AssistantMessage['message'], requestId: undefined, type: 'assistant', uuid: randomUUID(), @@ -387,9 +391,9 @@ export async function* queryModelOpenAI( // AssistantMessage + StreamEvent (matching the Anthropic path behavior) // Accumulate content blocks and usage, same as the Anthropic path in claude.ts - const contentBlocks: Record = {} + const contentBlocks: Record> = {} const collectedMessages: AssistantMessage[] = [] - let partialMessage: any + let partialMessage: BetaMessage | null = null let stopReason: string | null = null let usage = { input_tokens: 0, @@ -403,19 +407,19 @@ export async function* queryModelOpenAI( for await (const event of adaptedStream) { switch (event.type) { case 'message_start': { - partialMessage = (event as any).message + partialMessage = event.message ttftMs = Date.now() - start - if ((event as any).message?.usage) { + if (event.message.usage) { usage = { ...usage, - ...(event as any).message.usage, + ...(event.message.usage as unknown as typeof usage), } } break } case 'content_block_start': { - const idx = (event as any).index - const cb = (event as any).content_block + const idx = event.index + const cb = event.content_block if (cb.type === 'tool_use') { contentBlocks[idx] = { ...cb, input: '' } } else if (cb.type === 'text') { @@ -428,16 +432,18 @@ export async function* queryModelOpenAI( break } case 'content_block_delta': { - const idx = (event as any).index - const delta = (event as any).delta + const idx = event.index + const delta = event.delta const block = contentBlocks[idx] if (!block) break if (delta.type === 'text_delta') { - block.text = (block.text || '') + delta.text + block.text = ((block.text as string | undefined) || '') + delta.text } else if (delta.type === 'input_json_delta') { - block.input = (block.input || '') + delta.partial_json + block.input = + ((block.input as string | undefined) || '') + delta.partial_json } else if (delta.type === 'thinking_delta') { - block.thinking = (block.thinking || '') + delta.thinking + block.thinking = + ((block.thinking as string | undefined) || '') + delta.thinking } else if (delta.type === 'signature_delta') { block.signature = delta.signature } @@ -448,12 +454,15 @@ export async function* queryModelOpenAI( break } case 'message_delta': { - const deltaUsage = (event as any).usage + const deltaUsage = event.usage if (deltaUsage) { - usage = updateOpenAIUsage(usage, deltaUsage) + usage = updateOpenAIUsage( + usage, + deltaUsage as unknown as Parameters[1], + ) } - if ((event as any).delta?.stop_reason != null) { - stopReason = (event as any).delta.stop_reason + if (event.delta.stop_reason != null) { + stopReason = event.delta.stop_reason } break } @@ -482,8 +491,15 @@ export async function* queryModelOpenAI( } // Track cost and token usage if (usage.input_tokens + usage.output_tokens > 0) { - const costUSD = calculateUSDCost(openaiModel, usage as any) - addToTotalSessionCost(costUSD, usage as any, options.model) + const costUSD = calculateUSDCost( + openaiModel, + usage as unknown as BetaUsage, + ) + addToTotalSessionCost( + costUSD, + usage as unknown as BetaUsage, + options.model, + ) } break } diff --git a/src/utils/auth.ts b/src/utils/auth.ts index c640ed7a9..b650b704e 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -117,8 +117,8 @@ export function isAnthropicAuthEnabled(): boolean { isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) || isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) || isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) || - (settings as any).modelType === 'openai' || - (settings as any).modelType === 'gemini' || + settings.modelType === 'openai' || + settings.modelType === 'gemini' || !!process.env.OPENAI_BASE_URL || !!process.env.GEMINI_BASE_URL const apiKeyHelper = settings.apiKeyHelper diff --git a/src/utils/filePersistence/outputsScanner.ts b/src/utils/filePersistence/outputsScanner.ts index fbc4d8008..c46b2bb25 100644 --- a/src/utils/filePersistence/outputsScanner.ts +++ b/src/utils/filePersistence/outputsScanner.ts @@ -64,12 +64,14 @@ export async function findModifiedFiles( outputsDir: string, ): Promise { // Use recursive flag to get all entries in one call - let entries: Awaited> | any[] + let entries: + | Awaited> + | { name: string; isFile(): boolean; isSymbolicLink(): boolean }[] try { entries = (await fs.readdir(outputsDir, { withFileTypes: true, recursive: true, - })) as any[] + })) as { name: string; isFile(): boolean; isSymbolicLink(): boolean }[] } catch { // Directory doesn't exist or is not accessible return [] @@ -113,7 +115,7 @@ export async function findModifiedFiles( // Filter to files modified since turn start const modifiedFiles: string[] = [] for (const result of statResults) { - if (result && result.mtimeMs >= (turnStartTime as any as number)) { + if (result && result.mtimeMs >= turnStartTime.turnStartTime) { modifiedFiles.push(result.filePath) } } diff --git a/src/utils/messageQueueManager.ts b/src/utils/messageQueueManager.ts index 76f162f4a..be34d32ac 100644 --- a/src/utils/messageQueueManager.ts +++ b/src/utils/messageQueueManager.ts @@ -368,7 +368,9 @@ export function isQueuedCommandEditable(cmd: QueuedCommand): boolean { export function isQueuedCommandVisible(cmd: QueuedCommand): boolean { if ( (feature('KAIROS') || feature('KAIROS_CHANNELS')) && - (cmd as any).origin?.kind === 'channel' + (cmd as Record).origin !== undefined && + ((cmd as Record).origin as Record) + ?.kind === 'channel' ) return true return isQueuedCommandEditable(cmd) diff --git a/src/utils/performanceShim.ts b/src/utils/performanceShim.ts index 2044d74ee..c089f9edc 100644 --- a/src/utils/performanceShim.ts +++ b/src/utils/performanceShim.ts @@ -137,13 +137,14 @@ const shim = { (() => {}) as typeof performance.setResourceTimingBufferSize, // Node.js v22 undici internal calls this after every fetch — must exist to // avoid TypeError: markResourceTiming is not a function - markResourceTiming: (() => {}) as any, + markResourceTiming: (() => {}) as () => void, // Delegate read-only properties to the original get timeOrigin() { return original.timeOrigin }, get onresourcetimingbufferfull() { - return (original as any).onresourcetimingbufferfull + return (original as unknown as typeof performance) + .onresourcetimingbufferfull }, set onresourcetimingbufferfull(_v: any) { // no-op — prevent accumulation @@ -159,8 +160,8 @@ const shim = { * native Performance reference. */ export function installPerformanceShim(): void { - if ((globalThis as any).__performanceShimInstalled) return - ;(globalThis as any).__performanceShimInstalled = true + if ((globalThis as Record).__performanceShimInstalled) return + ;(globalThis as Record).__performanceShimInstalled = true globalThis.performance = shim } diff --git a/src/utils/plans.ts b/src/utils/plans.ts index e5f087b5c..914d7ab6e 100644 --- a/src/utils/plans.ts +++ b/src/utils/plans.ts @@ -366,19 +366,19 @@ export async function persistFileSnapshotIfRemote(): Promise { return } try { - const snapshotFiles: SystemFileSnapshotMessage['snapshotFiles'] = [] + const snapshotFiles: { key: string; path: string; content: string }[] = [] // Snapshot plan file const plan = getPlan() if (plan) { - ;(snapshotFiles as any[]).push({ + snapshotFiles.push({ key: 'plan', path: getPlanFilePath(), content: plan, }) } - if ((snapshotFiles as any[]).length === 0) { + if (snapshotFiles.length === 0) { return } diff --git a/src/utils/sliceAnsi.ts b/src/utils/sliceAnsi.ts index 7b7d25e18..dfbe6a830 100644 --- a/src/utils/sliceAnsi.ts +++ b/src/utils/sliceAnsi.ts @@ -1,5 +1,6 @@ import { type AnsiCode, + type Char, ansiCodesToString, reduceAnsiCodes, tokenize, @@ -83,7 +84,7 @@ export default function sliceAnsi( } if (include) { - result += (token as any).value + result += (token as Char).value } position += width diff --git a/src/utils/textHighlighting.ts b/src/utils/textHighlighting.ts index d0d24f7b5..e01726ea3 100644 --- a/src/utils/textHighlighting.ts +++ b/src/utils/textHighlighting.ts @@ -1,5 +1,6 @@ import { type AnsiCode, + type Char, ansiCodesToString, reduceAnsiCodes, type Token, @@ -128,14 +129,14 @@ class HighlightSegmenter { this.tokenIdx++ } else { const charsNeeded = targetVisiblePos - this.visiblePos - const charsAvailable = (token as any).value.length - this.charIdx + const charsAvailable = (token as Char).value.length - this.charIdx const charsToTake = Math.min(charsNeeded, charsAvailable) this.stringPos += charsToTake this.visiblePos += charsToTake this.charIdx += charsToTake - if (this.charIdx >= (token as any).value.length) { + if (this.charIdx >= (token as Char).value.length) { this.tokenIdx++ this.charIdx = 0 }