init claude-code

This commit is contained in:
2026-04-01 17:32:37 +02:00
commit 73b208c009
1902 changed files with 513237 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
import { c as _c } from "react/compiler-runtime";
import * as React from 'react';
import { Text, useTheme } from '../../ink.js';
import { getTheme, type Theme } from '../../utils/theme.js';
import { interpolateColor, parseRGB, toRGBColor } from './utils.js';
type Props = {
char: string;
flashOpacity: number;
messageColor: keyof Theme;
shimmerColor: keyof Theme;
};
export function FlashingChar(t0) {
const $ = _c(9);
const {
char,
flashOpacity,
messageColor,
shimmerColor
} = t0;
const [themeName] = useTheme();
let t1;
if ($[0] !== char || $[1] !== flashOpacity || $[2] !== messageColor || $[3] !== shimmerColor || $[4] !== themeName) {
t1 = Symbol.for("react.early_return_sentinel");
bb0: {
const theme = getTheme(themeName);
const baseColorStr = theme[messageColor];
const shimmerColorStr = theme[shimmerColor];
const baseRGB = baseColorStr ? parseRGB(baseColorStr) : null;
const shimmerRGB = shimmerColorStr ? parseRGB(shimmerColorStr) : null;
if (baseRGB && shimmerRGB) {
const interpolated = interpolateColor(baseRGB, shimmerRGB, flashOpacity);
t1 = <Text color={toRGBColor(interpolated)}>{char}</Text>;
break bb0;
}
}
$[0] = char;
$[1] = flashOpacity;
$[2] = messageColor;
$[3] = shimmerColor;
$[4] = themeName;
$[5] = t1;
} else {
t1 = $[5];
}
if (t1 !== Symbol.for("react.early_return_sentinel")) {
return t1;
}
const shouldUseShimmer = flashOpacity > 0.5;
const t2 = shouldUseShimmer ? shimmerColor : messageColor;
let t3;
if ($[6] !== char || $[7] !== t2) {
t3 = <Text color={t2}>{char}</Text>;
$[6] = char;
$[7] = t2;
$[8] = t3;
} else {
t3 = $[8];
}
return t3;
}
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIlRleHQiLCJ1c2VUaGVtZSIsImdldFRoZW1lIiwiVGhlbWUiLCJpbnRlcnBvbGF0ZUNvbG9yIiwicGFyc2VSR0IiLCJ0b1JHQkNvbG9yIiwiUHJvcHMiLCJjaGFyIiwiZmxhc2hPcGFjaXR5IiwibWVzc2FnZUNvbG9yIiwic2hpbW1lckNvbG9yIiwiRmxhc2hpbmdDaGFyIiwidDAiLCIkIiwiX2MiLCJ0aGVtZU5hbWUiLCJ0MSIsIlN5bWJvbCIsImZvciIsImJiMCIsInRoZW1lIiwiYmFzZUNvbG9yU3RyIiwic2hpbW1lckNvbG9yU3RyIiwiYmFzZVJHQiIsInNoaW1tZXJSR0IiLCJpbnRlcnBvbGF0ZWQiLCJzaG91bGRVc2VTaGltbWVyIiwidDIiLCJ0MyJdLCJzb3VyY2VzIjpbIkZsYXNoaW5nQ2hhci50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0ICogYXMgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBUZXh0LCB1c2VUaGVtZSB9IGZyb20gJy4uLy4uL2luay5qcydcbmltcG9ydCB7IGdldFRoZW1lLCB0eXBlIFRoZW1lIH0gZnJvbSAnLi4vLi4vdXRpbHMvdGhlbWUuanMnXG5pbXBvcnQgeyBpbnRlcnBvbGF0ZUNvbG9yLCBwYXJzZVJHQiwgdG9SR0JDb2xvciB9IGZyb20gJy4vdXRpbHMuanMnXG5cbnR5cGUgUHJvcHMgPSB7XG4gIGNoYXI6IHN0cmluZ1xuICBmbGFzaE9wYWNpdHk6IG51bWJlclxuICBtZXNzYWdlQ29sb3I6IGtleW9mIFRoZW1lXG4gIHNoaW1tZXJDb2xvcjoga2V5b2YgVGhlbWVcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIEZsYXNoaW5nQ2hhcih7XG4gIGNoYXIsXG4gIGZsYXNoT3BhY2l0eSxcbiAgbWVzc2FnZUNvbG9yLFxuICBzaGltbWVyQ29sb3IsXG59OiBQcm9wcyk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IFt0aGVtZU5hbWVdID0gdXNlVGhlbWUoKVxuICBjb25zdCB0aGVtZSA9IGdldFRoZW1lKHRoZW1lTmFtZSlcblxuICBjb25zdCBiYXNlQ29sb3JTdHIgPSB0aGVtZVttZXNzYWdlQ29sb3JdXG4gIGNvbnN0IHNoaW1tZXJDb2xvclN0ciA9IHRoZW1lW3NoaW1tZXJDb2xvcl1cblxuICBjb25zdCBiYXNlUkdCID0gYmFzZUNvbG9yU3RyID8gcGFyc2VSR0IoYmFzZUNvbG9yU3RyKSA6IG51bGxcbiAgY29uc3Qgc2hpbW1lclJHQiA9IHNoaW1tZXJDb2xvclN0ciA/IHBhcnNlUkdCKHNoaW1tZXJDb2xvclN0cikgOiBudWxsXG5cbiAgaWYgKGJhc2VSR0IgJiYgc2hpbW1lclJHQikge1xuICAgIC8vIFNtb290aCBpbnRlcnBvbGF0aW9uIGJldHdlZW4gY29sb3JzXG4gICAgY29uc3QgaW50ZXJwb2xhdGVkID0gaW50ZXJwb2xhdGVDb2xvcihiYXNlUkdCLCBzaGltbWVyUkdCLCBmbGFzaE9wYWNpdHkpXG4gICAgcmV0dXJuIDxUZXh0IGNvbG9yPXt0b1JHQkNvbG9yKGludGVycG9sYXRlZCl9PntjaGFyfTwvVGV4dD5cbiAgfVxuXG4gIC8vIEZhbGxiYWNrIGZvciBBTlNJIHRoZW1lczogYmluYXJ5IHN3aXRjaFxuICBjb25zdCBzaG91bGRVc2VTaGltbWVyID0gZmxhc2hPcGFjaXR5ID4gMC41XG4gIHJldHVybiAoXG4gICAgPFRleHQgY29sb3I9e3Nob3VsZFVzZVNoaW1tZXIgPyBzaGltbWVyQ29sb3IgOiBtZXNzYWdlQ29sb3J9PntjaGFyfTwvVGV4dD5cbiAgKVxufVxuIl0sIm1hcHBpbmdzIjoiO0FBQUEsT0FBTyxLQUFLQSxLQUFLLE1BQU0sT0FBTztBQUM5QixTQUFTQyxJQUFJLEVBQUVDLFFBQVEsUUFBUSxjQUFjO0FBQzdDLFNBQVNDLFFBQVEsRUFBRSxLQUFLQyxLQUFLLFFBQVEsc0JBQXNCO0FBQzNELFNBQVNDLGdCQUFnQixFQUFFQyxRQUFRLEVBQUVDLFVBQVUsUUFBUSxZQUFZO0FBRW5FLEtBQUtDLEtBQUssR0FBRztFQUNYQyxJQUFJLEVBQUUsTUFBTTtFQUNa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
File diff suppressed because one or more lines are too long
+36
View File
@@ -0,0 +1,36 @@
import { c as _c } from "react/compiler-runtime";
import * as React from 'react';
import { Text } from '../../ink.js';
import type { Theme } from '../../utils/theme.js';
type Props = {
char: string;
index: number;
glimmerIndex: number;
messageColor: keyof Theme;
shimmerColor: keyof Theme;
};
export function ShimmerChar(t0) {
const $ = _c(3);
const {
char,
index,
glimmerIndex,
messageColor,
shimmerColor
} = t0;
const isHighlighted = index === glimmerIndex;
const isNearHighlight = Math.abs(index - glimmerIndex) === 1;
const shouldUseShimmer = isHighlighted || isNearHighlight;
const t1 = shouldUseShimmer ? shimmerColor : messageColor;
let t2;
if ($[0] !== char || $[1] !== t1) {
t2 = <Text color={t1}>{char}</Text>;
$[0] = char;
$[1] = t1;
$[2] = t2;
} else {
t2 = $[2];
}
return t2;
}
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIlRleHQiLCJUaGVtZSIsIlByb3BzIiwiY2hhciIsImluZGV4IiwiZ2xpbW1lckluZGV4IiwibWVzc2FnZUNvbG9yIiwic2hpbW1lckNvbG9yIiwiU2hpbW1lckNoYXIiLCJ0MCIsIiQiLCJfYyIsImlzSGlnaGxpZ2h0ZWQiLCJpc05lYXJIaWdobGlnaHQiLCJNYXRoIiwiYWJzIiwic2hvdWxkVXNlU2hpbW1lciIsInQxIiwidDIiXSwic291cmNlcyI6WyJTaGltbWVyQ2hhci50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0ICogYXMgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBUZXh0IH0gZnJvbSAnLi4vLi4vaW5rLmpzJ1xuaW1wb3J0IHR5cGUgeyBUaGVtZSB9IGZyb20gJy4uLy4uL3V0aWxzL3RoZW1lLmpzJ1xuXG50eXBlIFByb3BzID0ge1xuICBjaGFyOiBzdHJpbmdcbiAgaW5kZXg6IG51bWJlclxuICBnbGltbWVySW5kZXg6IG51bWJlclxuICBtZXNzYWdlQ29sb3I6IGtleW9mIFRoZW1lXG4gIHNoaW1tZXJDb2xvcjoga2V5b2YgVGhlbWVcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIFNoaW1tZXJDaGFyKHtcbiAgY2hhcixcbiAgaW5kZXgsXG4gIGdsaW1tZXJJbmRleCxcbiAgbWVzc2FnZUNvbG9yLFxuICBzaGltbWVyQ29sb3IsXG59OiBQcm9wcyk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGNvbnN0IGlzSGlnaGxpZ2h0ZWQgPSBpbmRleCA9PT0gZ2xpbW1lckluZGV4XG4gIGNvbnN0IGlzTmVhckhpZ2hsaWdodCA9IE1hdGguYWJzKGluZGV4IC0gZ2xpbW1lckluZGV4KSA9PT0gMVxuICBjb25zdCBzaG91bGRVc2VTaGltbWVyID0gaXNIaWdobGlnaHRlZCB8fCBpc05lYXJIaWdobGlnaHRcblxuICByZXR1cm4gKFxuICAgIDxUZXh0IGNvbG9yPXtzaG91bGRVc2VTaGltbWVyID8gc2hpbW1lckNvbG9yIDogbWVzc2FnZUNvbG9yfT57Y2hhcn08L1RleHQ+XG4gIClcbn1cbiJdLCJtYXBwaW5ncyI6IjtBQUFBLE9BQU8sS0FBS0EsS0FBSyxNQUFNLE9BQU87QUFDOUIsU0FBU0MsSUFBSSxRQUFRLGNBQWM7QUFDbkMsY0FBY0MsS0FBSyxRQUFRLHNCQUFzQjtBQUVqRCxLQUFLQyxLQUFLLEdBQUc7RUFDWEMsSUFBSSxFQUFFLE1BQU07RUFDWkMsS0FBSyxFQUFFLE1BQU07RUFDYkMsWUFBWSxFQUFFLE1BQU07RUFDcEJDLFlBQVksRUFBRSxNQUFNTCxLQUFLO0VBQ3pCTSxZQUFZLEVBQUUsTUFBTU4sS0FBSztBQUMzQixDQUFDO0FBRUQsT0FBTyxTQUFBTyxZQUFBQyxFQUFBO0VBQUEsTUFBQUMsQ0FBQSxHQUFBQyxFQUFBO0VBQXFCO0lBQUFSLElBQUE7SUFBQUMsS0FBQTtJQUFBQyxZQUFBO0lBQUFDLFlBQUE7SUFBQUM7RUFBQSxJQUFBRSxFQU1wQjtFQUNOLE1BQUFHLGFBQUEsR0FBc0JSLEtBQUssS0FBS0MsWUFBWTtFQUM1QyxNQUFBUSxlQUFBLEdBQXdCQyxJQUFJLENBQUFDLEdBQUksQ0FBQ1gsS0FBSyxHQUFHQyxZQUFZLENBQUMsS0FBSyxDQUFDO0VBQzVELE1BQUFXLGdCQUFBLEdBQXlCSixhQUFnQyxJQUFoQ0MsZUFBZ0M7RUFHMUMsTUFBQUksRUFBQSxHQUFBRCxnQkFBZ0IsR0FBaEJULFlBQThDLEdBQTlDRCxZQUE4QztFQUFBLElBQUFZLEVBQUE7RUFBQSxJQUFBUixDQUFBLFFBQUFQLElBQUEsSUFBQU8sQ0FBQSxRQUFBTyxFQUFBO0lBQTNEQyxFQUFBLElBQUMsSUFBSSxDQUFRLEtBQThDLENBQTlDLENBQUFELEVBQTZDLENBQUMsQ0FBR2QsS0FBRyxDQUFFLEVBQWxFLElBQUksQ0FBcUU7SUFBQU8sQ0FBQSxNQUFBUCxJQUFBO0lBQUFPLENBQUEsTUFBQU8sRUFBQTtJQUFBUCxDQUFBLE1BQUFRLEVBQUE7RUFBQTtJQUFBQSxFQUFBLEdBQUFSLENBQUE7RUFBQTtFQUFBLE9BQTFFUSxFQUEwRTtBQUFBIiwiaWdub3JlTGlzdCI6W119
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+10
View File
@@ -0,0 +1,10 @@
export { FlashingChar } from './FlashingChar.js'
export { GlimmerMessage } from './GlimmerMessage.js'
export { ShimmerChar } from './ShimmerChar.js'
export { SpinnerGlyph } from './SpinnerGlyph.js'
export type { SpinnerMode } from './types.js'
export { useShimmerAnimation } from './useShimmerAnimation.js'
export { useStalledAnimation } from './useStalledAnimation.js'
export { getDefaultCharacters, interpolateColor } from './utils.js'
// Teammate components are NOT exported here - use dynamic require() to enable dead code elimination
// See REPL.tsx and Spinner.tsx for the correct import pattern
+1
View File
@@ -0,0 +1 @@
export const TEAMMATE_SELECT_HINT = 'shift + ↑/↓ to select'
+31
View File
@@ -0,0 +1,31 @@
import { useMemo } from 'react'
import { stringWidth } from '../../ink/stringWidth.js'
import { type DOMElement, useAnimationFrame } from '../../ink.js'
import type { SpinnerMode } from './types.js'
export function useShimmerAnimation(
mode: SpinnerMode,
message: string,
isStalled: boolean,
): [ref: (element: DOMElement | null) => void, glimmerIndex: number] {
const glimmerSpeed = mode === 'requesting' ? 50 : 200
// Pass null when stalled to unsubscribe from the clock — otherwise the
// setInterval keeps firing at 20fps even when the shimmer isn't visible.
// Notably, if the caller never attaches `ref` (e.g. conditional JSX),
// useTerminalViewport stays at its initial isVisible:true and the
// viewport-pause never kicks in, so this is the only stop mechanism.
const [ref, time] = useAnimationFrame(isStalled ? null : glimmerSpeed)
const messageWidth = useMemo(() => stringWidth(message), [message])
if (isStalled) {
return [ref, -100]
}
const cyclePosition = Math.floor(time / glimmerSpeed)
const cycleLength = messageWidth + 20
if (mode === 'requesting') {
return [ref, (cyclePosition % cycleLength) - 10]
}
return [ref, messageWidth + 10 - (cyclePosition % cycleLength)]
}
+75
View File
@@ -0,0 +1,75 @@
import { useRef } from 'react'
// Hook to handle the transition to red when tokens stop flowing.
// Driven by the parent's animation clock time instead of independent intervals,
// so it slows down when the terminal is blurred.
export function useStalledAnimation(
time: number,
currentResponseLength: number,
hasActiveTools = false,
reducedMotion = false,
): {
isStalled: boolean
stalledIntensity: number
} {
const lastTokenTime = useRef(time)
const lastResponseLength = useRef(currentResponseLength)
const mountTime = useRef(time)
const stalledIntensityRef = useRef(0)
const lastSmoothTime = useRef(time)
// Reset timer when new tokens arrive (check actual length change)
if (currentResponseLength > lastResponseLength.current) {
lastTokenTime.current = time
lastResponseLength.current = currentResponseLength
stalledIntensityRef.current = 0
lastSmoothTime.current = time
}
// Derive time since last token from animation clock
let timeSinceLastToken: number
if (hasActiveTools) {
timeSinceLastToken = 0
lastTokenTime.current = time
} else if (currentResponseLength > 0) {
timeSinceLastToken = time - lastTokenTime.current
} else {
timeSinceLastToken = time - mountTime.current
}
// Calculate stalled intensity based on time since last token
// Start showing red after 3 seconds of no new tokens (only when no tools are active)
const isStalled = timeSinceLastToken > 3000 && !hasActiveTools
const intensity = isStalled
? Math.min((timeSinceLastToken - 3000) / 2000, 1) // Fade over 2 seconds
: 0
// Smooth intensity transition driven by animation frame ticks
if (!reducedMotion && (intensity > 0 || stalledIntensityRef.current > 0)) {
const dt = time - lastSmoothTime.current
if (dt >= 50) {
const steps = Math.floor(dt / 50)
let current = stalledIntensityRef.current
for (let i = 0; i < steps; i++) {
const diff = intensity - current
if (Math.abs(diff) < 0.01) {
current = intensity
break
}
current += diff * 0.1
}
stalledIntensityRef.current = current
lastSmoothTime.current = time
}
} else {
stalledIntensityRef.current = intensity
lastSmoothTime.current = time
}
// When reducedMotion is enabled, use instant intensity change
const effectiveIntensity = reducedMotion
? intensity
: stalledIntensityRef.current
return { isStalled, stalledIntensity: effectiveIntensity }
}
+84
View File
@@ -0,0 +1,84 @@
import type { RGBColor as RGBColorString } from '../../ink/styles.js'
import type { RGBColor as RGBColorType } from './types.js'
export function getDefaultCharacters(): string[] {
if (process.env.TERM === 'xterm-ghostty') {
return ['·', '✢', '✳', '✶', '✻', '*'] // Use * instead of ✽ for Ghostty because the latter renders in a way that's slightly offset
}
return process.platform === 'darwin'
? ['·', '✢', '✳', '✶', '✻', '✽']
: ['·', '✢', '*', '✶', '✻', '✽']
}
// Interpolate between two RGB colors
export function interpolateColor(
color1: RGBColorType,
color2: RGBColorType,
t: number, // 0 to 1
): RGBColorType {
return {
r: Math.round(color1.r + (color2.r - color1.r) * t),
g: Math.round(color1.g + (color2.g - color1.g) * t),
b: Math.round(color1.b + (color2.b - color1.b) * t),
}
}
// Convert RGB object to rgb() color string for Text component
export function toRGBColor(color: RGBColorType): RGBColorString {
return `rgb(${color.r},${color.g},${color.b})`
}
// HSL hue (0-360) to RGB, using voice-mode waveform parameters (s=0.7, l=0.6).
export function hueToRgb(hue: number): RGBColorType {
const h = ((hue % 360) + 360) % 360
const s = 0.7
const l = 0.6
const c = (1 - Math.abs(2 * l - 1)) * s
const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
const m = l - c / 2
let r = 0
let g = 0
let b = 0
if (h < 60) {
r = c
g = x
} else if (h < 120) {
r = x
g = c
} else if (h < 180) {
g = c
b = x
} else if (h < 240) {
g = x
b = c
} else if (h < 300) {
r = x
b = c
} else {
r = c
b = x
}
return {
r: Math.round((r + m) * 255),
g: Math.round((g + m) * 255),
b: Math.round((b + m) * 255),
}
}
const RGB_CACHE = new Map<string, RGBColorType | null>()
export function parseRGB(colorStr: string): RGBColorType | null {
const cached = RGB_CACHE.get(colorStr)
if (cached !== undefined) return cached
const match = colorStr.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/)
const result = match
? {
r: parseInt(match[1]!, 10),
g: parseInt(match[2]!, 10),
b: parseInt(match[3]!, 10),
}
: null
RGB_CACHE.set(colorStr, result)
return result
}