mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 06:15:51 +00:00
更新大量 tsx 原始文件; 已经迁移 login panel; 部分 (#121)
* style(B1-1): 格式化 ink/buddy/cli/context/screens/tasks/services/keybindings/state (43 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 修复了 Box.tsx 和 ScrollBox.tsx 中无效的 global.d.ts import。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-2): 格式化 commands (79 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-3): 格式化 components/messages,permissions,mcp,sandbox,shell (104 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-4): 格式化 components/PromptInput,FeedbackSurvey,tasks,agents,skills,design-system,wizard (73 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-5): 格式化 components其余 + hooks + tools (232 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-6): 格式化 main/entrypoints/utils/moreright (21 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: 更新 README,新增 Run.ps1/TODO.md,删除 V6.md - README.md: 大幅重写,更详细版本历史和配置示例 - Run.ps1: 新增 Windows 启动脚本 - TODO.md: 新增包完成清单 - V6.md: 删除(架构重构规划已不适用) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: 修复以前的问题 * fix: 修复 login 面板的问题 --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,60 +1,39 @@
|
||||
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';
|
||||
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;
|
||||
char: string
|
||||
flashOpacity: number
|
||||
messageColor: keyof Theme
|
||||
shimmerColor: keyof Theme
|
||||
}
|
||||
|
||||
export function FlashingChar({
|
||||
char,
|
||||
flashOpacity,
|
||||
messageColor,
|
||||
shimmerColor,
|
||||
}: Props): React.ReactNode {
|
||||
const [themeName] = useTheme()
|
||||
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) {
|
||||
// Smooth interpolation between colors
|
||||
const interpolated = interpolateColor(baseRGB, shimmerRGB, flashOpacity)
|
||||
return <Text color={toRGBColor(interpolated)}>{char}</Text>
|
||||
}
|
||||
|
||||
// Fallback for ANSI themes: binary switch
|
||||
const shouldUseShimmer = flashOpacity > 0.5
|
||||
return (
|
||||
<Text color={shouldUseShimmer ? shimmerColor : messageColor}>{char}</Text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,327 +1,142 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { stringWidth } from '../../ink/stringWidth.js';
|
||||
import { Text, useTheme } from '../../ink.js';
|
||||
import { getGraphemeSegmenter } from '../../utils/intl.js';
|
||||
import { getTheme, type Theme } from '../../utils/theme.js';
|
||||
import type { SpinnerMode } from './types.js';
|
||||
import { interpolateColor, parseRGB, toRGBColor } from './utils.js';
|
||||
import * as React from 'react'
|
||||
import { stringWidth } from '../../ink/stringWidth.js'
|
||||
import { Text, useTheme } from '../../ink.js'
|
||||
import { getGraphemeSegmenter } from '../../utils/intl.js'
|
||||
import { getTheme, type Theme } from '../../utils/theme.js'
|
||||
import type { SpinnerMode } from './types.js'
|
||||
import { interpolateColor, parseRGB, toRGBColor } from './utils.js'
|
||||
|
||||
type Props = {
|
||||
message: string;
|
||||
mode: SpinnerMode;
|
||||
messageColor: keyof Theme;
|
||||
glimmerIndex: number;
|
||||
flashOpacity: number;
|
||||
shimmerColor: keyof Theme;
|
||||
stalledIntensity?: number;
|
||||
};
|
||||
const ERROR_RED = {
|
||||
r: 171,
|
||||
g: 43,
|
||||
b: 63
|
||||
};
|
||||
export function GlimmerMessage(t0) {
|
||||
const $ = _c(75);
|
||||
const {
|
||||
message,
|
||||
mode,
|
||||
messageColor,
|
||||
glimmerIndex,
|
||||
flashOpacity,
|
||||
shimmerColor,
|
||||
stalledIntensity: t1
|
||||
} = t0;
|
||||
const stalledIntensity = t1 === undefined ? 0 : t1;
|
||||
const [themeName] = useTheme();
|
||||
let messageWidth;
|
||||
let segments;
|
||||
let t2;
|
||||
if ($[0] !== flashOpacity || $[1] !== message || $[2] !== messageColor || $[3] !== mode || $[4] !== shimmerColor || $[5] !== stalledIntensity || $[6] !== themeName) {
|
||||
t2 = Symbol.for("react.early_return_sentinel");
|
||||
bb0: {
|
||||
const theme = getTheme(themeName);
|
||||
let segs;
|
||||
if ($[10] !== message) {
|
||||
segs = [];
|
||||
for (const {
|
||||
segment
|
||||
} of getGraphemeSegmenter().segment(message)) {
|
||||
segs.push({
|
||||
segment,
|
||||
width: stringWidth(segment)
|
||||
});
|
||||
}
|
||||
$[10] = message;
|
||||
$[11] = segs;
|
||||
} else {
|
||||
segs = $[11];
|
||||
}
|
||||
let t3;
|
||||
if ($[12] !== message) {
|
||||
t3 = stringWidth(message);
|
||||
$[12] = message;
|
||||
$[13] = t3;
|
||||
} else {
|
||||
t3 = $[13];
|
||||
}
|
||||
let t4;
|
||||
if ($[14] !== segs || $[15] !== t3) {
|
||||
t4 = {
|
||||
segments: segs,
|
||||
messageWidth: t3
|
||||
};
|
||||
$[14] = segs;
|
||||
$[15] = t3;
|
||||
$[16] = t4;
|
||||
} else {
|
||||
t4 = $[16];
|
||||
}
|
||||
({
|
||||
segments,
|
||||
messageWidth
|
||||
} = t4);
|
||||
if (!message) {
|
||||
t2 = null;
|
||||
break bb0;
|
||||
}
|
||||
if (stalledIntensity > 0) {
|
||||
const baseColorStr = theme[messageColor];
|
||||
const baseRGB = baseColorStr ? parseRGB(baseColorStr) : null;
|
||||
if (baseRGB) {
|
||||
const interpolated = interpolateColor(baseRGB, ERROR_RED, stalledIntensity);
|
||||
const color = toRGBColor(interpolated);
|
||||
let t5;
|
||||
if ($[17] !== color) {
|
||||
t5 = <Text color={color}> </Text>;
|
||||
$[17] = color;
|
||||
$[18] = t5;
|
||||
} else {
|
||||
t5 = $[18];
|
||||
}
|
||||
t2 = <><Text color={color}>{message}</Text>{t5}</>;
|
||||
break bb0;
|
||||
}
|
||||
const color_0 = stalledIntensity > 0.5 ? "error" : messageColor;
|
||||
let t5;
|
||||
if ($[19] !== color_0 || $[20] !== message) {
|
||||
t5 = <Text color={color_0}>{message}</Text>;
|
||||
$[19] = color_0;
|
||||
$[20] = message;
|
||||
$[21] = t5;
|
||||
} else {
|
||||
t5 = $[21];
|
||||
}
|
||||
let t6;
|
||||
if ($[22] !== color_0) {
|
||||
t6 = <Text color={color_0}> </Text>;
|
||||
$[22] = color_0;
|
||||
$[23] = t6;
|
||||
} else {
|
||||
t6 = $[23];
|
||||
}
|
||||
let t7;
|
||||
if ($[24] !== t5 || $[25] !== t6) {
|
||||
t7 = <>{t5}{t6}</>;
|
||||
$[24] = t5;
|
||||
$[25] = t6;
|
||||
$[26] = t7;
|
||||
} else {
|
||||
t7 = $[26];
|
||||
}
|
||||
t2 = t7;
|
||||
break bb0;
|
||||
}
|
||||
if (mode === "tool-use") {
|
||||
const baseColorStr_0 = theme[messageColor];
|
||||
const shimmerColorStr = theme[shimmerColor];
|
||||
const baseRGB_0 = baseColorStr_0 ? parseRGB(baseColorStr_0) : null;
|
||||
const shimmerRGB = shimmerColorStr ? parseRGB(shimmerColorStr) : null;
|
||||
if (baseRGB_0 && shimmerRGB) {
|
||||
const interpolated_0 = interpolateColor(baseRGB_0, shimmerRGB, flashOpacity);
|
||||
const t5 = <Text color={toRGBColor(interpolated_0)}>{message}</Text>;
|
||||
let t6;
|
||||
if ($[27] !== messageColor) {
|
||||
t6 = <Text color={messageColor}> </Text>;
|
||||
$[27] = messageColor;
|
||||
$[28] = t6;
|
||||
} else {
|
||||
t6 = $[28];
|
||||
}
|
||||
let t7;
|
||||
if ($[29] !== t5 || $[30] !== t6) {
|
||||
t7 = <>{t5}{t6}</>;
|
||||
$[29] = t5;
|
||||
$[30] = t6;
|
||||
$[31] = t7;
|
||||
} else {
|
||||
t7 = $[31];
|
||||
}
|
||||
t2 = t7;
|
||||
break bb0;
|
||||
}
|
||||
const color_1 = flashOpacity > 0.5 ? shimmerColor : messageColor;
|
||||
let t5;
|
||||
if ($[32] !== color_1 || $[33] !== message) {
|
||||
t5 = <Text color={color_1}>{message}</Text>;
|
||||
$[32] = color_1;
|
||||
$[33] = message;
|
||||
$[34] = t5;
|
||||
} else {
|
||||
t5 = $[34];
|
||||
}
|
||||
let t6;
|
||||
if ($[35] !== messageColor) {
|
||||
t6 = <Text color={messageColor}> </Text>;
|
||||
$[35] = messageColor;
|
||||
$[36] = t6;
|
||||
} else {
|
||||
t6 = $[36];
|
||||
}
|
||||
let t7;
|
||||
if ($[37] !== t5 || $[38] !== t6) {
|
||||
t7 = <>{t5}{t6}</>;
|
||||
$[37] = t5;
|
||||
$[38] = t6;
|
||||
$[39] = t7;
|
||||
} else {
|
||||
t7 = $[39];
|
||||
}
|
||||
t2 = t7;
|
||||
break bb0;
|
||||
}
|
||||
}
|
||||
$[0] = flashOpacity;
|
||||
$[1] = message;
|
||||
$[2] = messageColor;
|
||||
$[3] = mode;
|
||||
$[4] = shimmerColor;
|
||||
$[5] = stalledIntensity;
|
||||
$[6] = themeName;
|
||||
$[7] = messageWidth;
|
||||
$[8] = segments;
|
||||
$[9] = t2;
|
||||
} else {
|
||||
messageWidth = $[7];
|
||||
segments = $[8];
|
||||
t2 = $[9];
|
||||
}
|
||||
if (t2 !== Symbol.for("react.early_return_sentinel")) {
|
||||
return t2;
|
||||
}
|
||||
const shimmerStart = glimmerIndex - 1;
|
||||
const shimmerEnd = glimmerIndex + 1;
|
||||
if (shimmerStart >= messageWidth || shimmerEnd < 0) {
|
||||
let t3;
|
||||
if ($[40] !== message || $[41] !== messageColor) {
|
||||
t3 = <Text color={messageColor}>{message}</Text>;
|
||||
$[40] = message;
|
||||
$[41] = messageColor;
|
||||
$[42] = t3;
|
||||
} else {
|
||||
t3 = $[42];
|
||||
}
|
||||
let t4;
|
||||
if ($[43] !== messageColor) {
|
||||
t4 = <Text color={messageColor}> </Text>;
|
||||
$[43] = messageColor;
|
||||
$[44] = t4;
|
||||
} else {
|
||||
t4 = $[44];
|
||||
}
|
||||
let t5;
|
||||
if ($[45] !== t3 || $[46] !== t4) {
|
||||
t5 = <>{t3}{t4}</>;
|
||||
$[45] = t3;
|
||||
$[46] = t4;
|
||||
$[47] = t5;
|
||||
} else {
|
||||
t5 = $[47];
|
||||
}
|
||||
return t5;
|
||||
}
|
||||
const clampedStart = Math.max(0, shimmerStart);
|
||||
let colPos = 0;
|
||||
let before = "";
|
||||
let shim = "";
|
||||
let after = "";
|
||||
if ($[48] !== after || $[49] !== before || $[50] !== clampedStart || $[51] !== colPos || $[52] !== segments || $[53] !== shim || $[54] !== shimmerEnd) {
|
||||
for (const {
|
||||
segment: segment_0,
|
||||
width
|
||||
} of segments) {
|
||||
if (colPos + width <= clampedStart) {
|
||||
before = before + segment_0;
|
||||
} else {
|
||||
if (colPos > shimmerEnd) {
|
||||
after = after + segment_0;
|
||||
} else {
|
||||
shim = shim + segment_0;
|
||||
}
|
||||
}
|
||||
colPos = colPos + width;
|
||||
}
|
||||
$[48] = after;
|
||||
$[49] = before;
|
||||
$[50] = clampedStart;
|
||||
$[51] = colPos;
|
||||
$[52] = segments;
|
||||
$[53] = shim;
|
||||
$[54] = shimmerEnd;
|
||||
$[55] = before;
|
||||
$[56] = after;
|
||||
$[57] = shim;
|
||||
$[58] = colPos;
|
||||
} else {
|
||||
before = $[55] as string;
|
||||
after = $[56] as string;
|
||||
shim = $[57] as string;
|
||||
colPos = $[58] as number;
|
||||
}
|
||||
let t3;
|
||||
if ($[59] !== before || $[60] !== messageColor) {
|
||||
t3 = before && <Text color={messageColor}>{before}</Text>;
|
||||
$[59] = before;
|
||||
$[60] = messageColor;
|
||||
$[61] = t3;
|
||||
} else {
|
||||
t3 = $[61];
|
||||
}
|
||||
let t4;
|
||||
if ($[62] !== shim || $[63] !== shimmerColor) {
|
||||
t4 = <Text color={shimmerColor}>{shim}</Text>;
|
||||
$[62] = shim;
|
||||
$[63] = shimmerColor;
|
||||
$[64] = t4;
|
||||
} else {
|
||||
t4 = $[64];
|
||||
}
|
||||
let t5;
|
||||
if ($[65] !== after || $[66] !== messageColor) {
|
||||
t5 = after && <Text color={messageColor}>{after}</Text>;
|
||||
$[65] = after;
|
||||
$[66] = messageColor;
|
||||
$[67] = t5;
|
||||
} else {
|
||||
t5 = $[67];
|
||||
}
|
||||
let t6;
|
||||
if ($[68] !== messageColor) {
|
||||
t6 = <Text color={messageColor}> </Text>;
|
||||
$[68] = messageColor;
|
||||
$[69] = t6;
|
||||
} else {
|
||||
t6 = $[69];
|
||||
}
|
||||
let t7;
|
||||
if ($[70] !== t3 || $[71] !== t4 || $[72] !== t5 || $[73] !== t6) {
|
||||
t7 = <>{t3}{t4}{t5}{t6}</>;
|
||||
$[70] = t3;
|
||||
$[71] = t4;
|
||||
$[72] = t5;
|
||||
$[73] = t6;
|
||||
$[74] = t7;
|
||||
} else {
|
||||
t7 = $[74];
|
||||
}
|
||||
return t7;
|
||||
message: string
|
||||
mode: SpinnerMode
|
||||
messageColor: keyof Theme
|
||||
glimmerIndex: number
|
||||
flashOpacity: number
|
||||
shimmerColor: keyof Theme
|
||||
stalledIntensity?: number
|
||||
}
|
||||
|
||||
const ERROR_RED = { r: 171, g: 43, b: 63 }
|
||||
|
||||
export function GlimmerMessage({
|
||||
message,
|
||||
mode,
|
||||
messageColor,
|
||||
glimmerIndex,
|
||||
flashOpacity,
|
||||
shimmerColor,
|
||||
stalledIntensity = 0,
|
||||
}: Props): React.ReactNode {
|
||||
const [themeName] = useTheme()
|
||||
const theme = getTheme(themeName)
|
||||
|
||||
// This component re-renders at 20fps (glimmerIndex changes every 50ms) but
|
||||
// message is stable within a turn. Precompute grapheme segmentation + widths
|
||||
// once per message instead of per frame. Measured -81% on the shimmer path.
|
||||
const { segments, messageWidth } = React.useMemo(() => {
|
||||
const segs: { segment: string; width: number }[] = []
|
||||
for (const { segment } of getGraphemeSegmenter().segment(message)) {
|
||||
segs.push({ segment, width: stringWidth(segment) })
|
||||
}
|
||||
return { segments: segs, messageWidth: stringWidth(message) }
|
||||
}, [message])
|
||||
|
||||
if (!message) return null
|
||||
|
||||
// When stalled, show text that smoothly transitions to red
|
||||
if (stalledIntensity > 0) {
|
||||
const baseColorStr = theme[messageColor]
|
||||
const baseRGB = baseColorStr ? parseRGB(baseColorStr) : null
|
||||
|
||||
if (baseRGB) {
|
||||
const interpolated = interpolateColor(
|
||||
baseRGB,
|
||||
ERROR_RED,
|
||||
stalledIntensity,
|
||||
)
|
||||
const color = toRGBColor(interpolated)
|
||||
return (
|
||||
<>
|
||||
<Text color={color}>{message}</Text>
|
||||
<Text color={color}> </Text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Fallback for ANSI themes: use messageColor until fully stalled, then error
|
||||
const color = stalledIntensity > 0.5 ? 'error' : messageColor
|
||||
return (
|
||||
<>
|
||||
<Text color={color}>{message}</Text>
|
||||
<Text color={color}> </Text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// tool-use mode: all chars flash with the same opacity, so render as a
|
||||
// single <Text> instead of N individual FlashingChar components.
|
||||
if (mode === 'tool-use') {
|
||||
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)
|
||||
return (
|
||||
<>
|
||||
<Text color={toRGBColor(interpolated)}>{message}</Text>
|
||||
<Text color={messageColor}> </Text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const color = flashOpacity > 0.5 ? shimmerColor : messageColor
|
||||
return (
|
||||
<>
|
||||
<Text color={color}>{message}</Text>
|
||||
<Text color={messageColor}> </Text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Shimmer mode: only chars within ±1 of glimmerIndex need the shimmer
|
||||
// color. When glimmer is offscreen, render as a single <Text>.
|
||||
const shimmerStart = glimmerIndex - 1
|
||||
const shimmerEnd = glimmerIndex + 1
|
||||
|
||||
if (shimmerStart >= messageWidth || shimmerEnd < 0) {
|
||||
return (
|
||||
<>
|
||||
<Text color={messageColor}>{message}</Text>
|
||||
<Text color={messageColor}> </Text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Split into at most 3 segments by visual column position
|
||||
const clampedStart = Math.max(0, shimmerStart)
|
||||
let colPos = 0
|
||||
let before = ''
|
||||
let shim = ''
|
||||
let after = ''
|
||||
for (const { segment, width } of segments) {
|
||||
if (colPos + width <= clampedStart) {
|
||||
before += segment
|
||||
} else if (colPos > shimmerEnd) {
|
||||
after += segment
|
||||
} else {
|
||||
shim += segment
|
||||
}
|
||||
colPos += width
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{before && <Text color={messageColor}>{before}</Text>}
|
||||
<Text color={shimmerColor}>{shim}</Text>
|
||||
{after && <Text color={messageColor}>{after}</Text>}
|
||||
<Text color={messageColor}> </Text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,35 +1,27 @@
|
||||
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';
|
||||
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;
|
||||
char: string
|
||||
index: number
|
||||
glimmerIndex: number
|
||||
messageColor: keyof Theme
|
||||
shimmerColor: keyof Theme
|
||||
}
|
||||
|
||||
export function ShimmerChar({
|
||||
char,
|
||||
index,
|
||||
glimmerIndex,
|
||||
messageColor,
|
||||
shimmerColor,
|
||||
}: Props): React.ReactNode {
|
||||
const isHighlighted = index === glimmerIndex
|
||||
const isNearHighlight = Math.abs(index - glimmerIndex) === 1
|
||||
const shouldUseShimmer = isHighlighted || isNearHighlight
|
||||
|
||||
return (
|
||||
<Text color={shouldUseShimmer ? shimmerColor : messageColor}>{char}</Text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,72 +1,66 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { stringWidth } from '../../ink/stringWidth.js';
|
||||
import { Box, Text, useAnimationFrame } from '../../ink.js';
|
||||
import type { InProcessTeammateTaskState } from '../../tasks/InProcessTeammateTask/types.js';
|
||||
import { formatDuration, formatNumber } from '../../utils/format.js';
|
||||
import { toInkColor } from '../../utils/ink.js';
|
||||
import type { Theme } from '../../utils/theme.js';
|
||||
import { Byline } from '../design-system/Byline.js';
|
||||
import { GlimmerMessage } from './GlimmerMessage.js';
|
||||
import { SpinnerGlyph } from './SpinnerGlyph.js';
|
||||
import type { SpinnerMode } from './types.js';
|
||||
import { useStalledAnimation } from './useStalledAnimation.js';
|
||||
import { interpolateColor, toRGBColor } from './utils.js';
|
||||
const SEP_WIDTH = stringWidth(' · ');
|
||||
const THINKING_BARE_WIDTH = stringWidth('thinking');
|
||||
const SHOW_TOKENS_AFTER_MS = 30_000;
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { stringWidth } from '../../ink/stringWidth.js'
|
||||
import { Box, Text, useAnimationFrame } from '../../ink.js'
|
||||
import type { InProcessTeammateTaskState } from '../../tasks/InProcessTeammateTask/types.js'
|
||||
import { formatDuration, formatNumber } from '../../utils/format.js'
|
||||
import { toInkColor } from '../../utils/ink.js'
|
||||
import type { Theme } from '../../utils/theme.js'
|
||||
import { Byline } from '../design-system/Byline.js'
|
||||
import { GlimmerMessage } from './GlimmerMessage.js'
|
||||
import { SpinnerGlyph } from './SpinnerGlyph.js'
|
||||
import type { SpinnerMode } from './types.js'
|
||||
import { useStalledAnimation } from './useStalledAnimation.js'
|
||||
import { interpolateColor, toRGBColor } from './utils.js'
|
||||
|
||||
const SEP_WIDTH = stringWidth(' · ')
|
||||
const THINKING_BARE_WIDTH = stringWidth('thinking')
|
||||
const SHOW_TOKENS_AFTER_MS = 30_000
|
||||
|
||||
// Thinking shimmer constants. Previously lived in a separate ThinkingShimmerText
|
||||
// component with its own useAnimationFrame(50) — inlined here to reuse our
|
||||
// existing 50ms clock and eliminate the redundant subscriber.
|
||||
const THINKING_INACTIVE = {
|
||||
r: 153,
|
||||
g: 153,
|
||||
b: 153
|
||||
};
|
||||
const THINKING_INACTIVE_SHIMMER = {
|
||||
r: 185,
|
||||
g: 185,
|
||||
b: 185
|
||||
};
|
||||
const THINKING_DELAY_MS = 3000;
|
||||
const THINKING_GLOW_PERIOD_S = 2;
|
||||
const THINKING_INACTIVE = { r: 153, g: 153, b: 153 }
|
||||
const THINKING_INACTIVE_SHIMMER = { r: 185, g: 185, b: 185 }
|
||||
const THINKING_DELAY_MS = 3000
|
||||
const THINKING_GLOW_PERIOD_S = 2
|
||||
|
||||
export type SpinnerAnimationRowProps = {
|
||||
// Animation inputs
|
||||
mode: SpinnerMode;
|
||||
reducedMotion: boolean;
|
||||
hasActiveTools: boolean;
|
||||
responseLengthRef: React.RefObject<number>;
|
||||
mode: SpinnerMode
|
||||
reducedMotion: boolean
|
||||
hasActiveTools: boolean
|
||||
responseLengthRef: React.RefObject<number>
|
||||
|
||||
// Message (stable within a turn)
|
||||
message: string;
|
||||
messageColor: keyof Theme;
|
||||
shimmerColor: keyof Theme;
|
||||
overrideColor?: keyof Theme | null;
|
||||
message: string
|
||||
messageColor: keyof Theme
|
||||
shimmerColor: keyof Theme
|
||||
overrideColor?: keyof Theme | null
|
||||
|
||||
// Timer refs (stable references)
|
||||
loadingStartTimeRef: React.RefObject<number>;
|
||||
totalPausedMsRef: React.RefObject<number>;
|
||||
pauseStartTimeRef: React.RefObject<number | null>;
|
||||
loadingStartTimeRef: React.RefObject<number>
|
||||
totalPausedMsRef: React.RefObject<number>
|
||||
pauseStartTimeRef: React.RefObject<number | null>
|
||||
|
||||
// Display flags
|
||||
spinnerSuffix?: string | null;
|
||||
verbose: boolean;
|
||||
columns: number;
|
||||
spinnerSuffix?: string | null
|
||||
verbose: boolean
|
||||
columns: number
|
||||
|
||||
// Teammate-derived (computed by parent from tasks)
|
||||
hasRunningTeammates: boolean;
|
||||
teammateTokens: number;
|
||||
foregroundedTeammate: InProcessTeammateTaskState | undefined;
|
||||
hasRunningTeammates: boolean
|
||||
teammateTokens: number
|
||||
foregroundedTeammate: InProcessTeammateTaskState | undefined
|
||||
/** Leader's turn has completed. Suppresses stall-red since responseLengthRef/hasActiveTools track leader state only. */
|
||||
leaderIsIdle?: boolean;
|
||||
leaderIsIdle?: boolean
|
||||
|
||||
// Thinking (state owned by parent, mode-dependent)
|
||||
thinkingStatus: 'thinking' | number | null;
|
||||
effortSuffix: string;
|
||||
};
|
||||
thinkingStatus: 'thinking' | number | null
|
||||
effortSuffix: string
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The 50ms-animated portion of SpinnerWithVerb. Owns useAnimationFrame(50)
|
||||
@@ -98,167 +92,282 @@ export function SpinnerAnimationRow({
|
||||
foregroundedTeammate,
|
||||
leaderIsIdle = false,
|
||||
thinkingStatus,
|
||||
effortSuffix
|
||||
effortSuffix,
|
||||
}: SpinnerAnimationRowProps): React.ReactNode {
|
||||
const [viewportRef, time] = useAnimationFrame(reducedMotion ? null : 50);
|
||||
const [viewportRef, time] = useAnimationFrame(reducedMotion ? null : 50)
|
||||
|
||||
// === Elapsed time (wall-clock, derived from refs each frame) ===
|
||||
const now = Date.now();
|
||||
const elapsedTimeMs = pauseStartTimeRef.current !== null ? pauseStartTimeRef.current - loadingStartTimeRef.current - totalPausedMsRef.current : now - loadingStartTimeRef.current - totalPausedMsRef.current;
|
||||
const now = Date.now()
|
||||
const elapsedTimeMs =
|
||||
pauseStartTimeRef.current !== null
|
||||
? pauseStartTimeRef.current -
|
||||
loadingStartTimeRef.current -
|
||||
totalPausedMsRef.current
|
||||
: now - loadingStartTimeRef.current - totalPausedMsRef.current
|
||||
|
||||
// Track wall-clock turn start for teammates. While a swarm is running the
|
||||
// leader's elapsedTimeMs may jump around (new API calls reset
|
||||
// loadingStartTimeRef; pauses freeze it), so we anchor to the earliest
|
||||
// derived start seen so far. When no teammates are running this just tracks
|
||||
// derivedStart every frame, effectively resetting for the next swarm.
|
||||
const derivedStart = now - elapsedTimeMs;
|
||||
const turnStartRef = useRef(derivedStart);
|
||||
const derivedStart = now - elapsedTimeMs
|
||||
const turnStartRef = useRef(derivedStart)
|
||||
if (!hasRunningTeammates || derivedStart < turnStartRef.current) {
|
||||
turnStartRef.current = derivedStart;
|
||||
turnStartRef.current = derivedStart
|
||||
}
|
||||
|
||||
// === Animation derivations from `time` ===
|
||||
const currentResponseLength = responseLengthRef.current;
|
||||
const currentResponseLength = responseLengthRef.current
|
||||
|
||||
// Suppress stall detection when leader is idle — responseLengthRef and
|
||||
// hasActiveTools both track leader state. When viewing an active teammate
|
||||
// while leader is idle, they'd otherwise flag a false stall after 3s.
|
||||
// Treating leaderIsIdle like hasActiveTools resets the stall timer.
|
||||
const {
|
||||
isStalled,
|
||||
stalledIntensity
|
||||
} = useStalledAnimation(time, currentResponseLength, hasActiveTools || leaderIsIdle, reducedMotion);
|
||||
const frame = reducedMotion ? 0 : Math.floor(time / 120);
|
||||
const glimmerSpeed = mode === 'requesting' ? 50 : 200;
|
||||
const { isStalled, stalledIntensity } = useStalledAnimation(
|
||||
time,
|
||||
currentResponseLength,
|
||||
hasActiveTools || leaderIsIdle,
|
||||
reducedMotion,
|
||||
)
|
||||
|
||||
const frame = reducedMotion ? 0 : Math.floor(time / 120)
|
||||
|
||||
const glimmerSpeed = mode === 'requesting' ? 50 : 200
|
||||
// message is stable within a turn; stringWidth is expensive enough (Bun native
|
||||
// call per code point) to memoize explicitly across the 50ms loop.
|
||||
const glimmerMessageWidth = useMemo(() => stringWidth(message), [message]);
|
||||
const cycleLength = glimmerMessageWidth + 20;
|
||||
const cyclePosition = Math.floor(time / glimmerSpeed);
|
||||
const glimmerIndex = reducedMotion ? -100 : isStalled ? -100 : mode === 'requesting' ? cyclePosition % cycleLength - 10 : glimmerMessageWidth + 10 - cyclePosition % cycleLength;
|
||||
const flashOpacity = reducedMotion ? 0 : mode === 'tool-use' ? (Math.sin(time / 1000 * Math.PI) + 1) / 2 : 0;
|
||||
const glimmerMessageWidth = useMemo(() => stringWidth(message), [message])
|
||||
const cycleLength = glimmerMessageWidth + 20
|
||||
const cyclePosition = Math.floor(time / glimmerSpeed)
|
||||
const glimmerIndex = reducedMotion
|
||||
? -100
|
||||
: isStalled
|
||||
? -100
|
||||
: mode === 'requesting'
|
||||
? (cyclePosition % cycleLength) - 10
|
||||
: glimmerMessageWidth + 10 - (cyclePosition % cycleLength)
|
||||
|
||||
const flashOpacity = reducedMotion
|
||||
? 0
|
||||
: mode === 'tool-use'
|
||||
? (Math.sin((time / 1000) * Math.PI) + 1) / 2
|
||||
: 0
|
||||
|
||||
// === Token counter animation (smooth increment, driven by 50ms clock) ===
|
||||
const tokenCounterRef = useRef(currentResponseLength);
|
||||
const tokenCounterRef = useRef(currentResponseLength)
|
||||
if (reducedMotion) {
|
||||
tokenCounterRef.current = currentResponseLength;
|
||||
tokenCounterRef.current = currentResponseLength
|
||||
} else {
|
||||
const gap = currentResponseLength - tokenCounterRef.current;
|
||||
const gap = currentResponseLength - tokenCounterRef.current
|
||||
if (gap > 0) {
|
||||
let increment;
|
||||
let increment
|
||||
if (gap < 70) {
|
||||
increment = 3;
|
||||
increment = 3
|
||||
} else if (gap < 200) {
|
||||
increment = Math.max(8, Math.ceil(gap * 0.15));
|
||||
increment = Math.max(8, Math.ceil(gap * 0.15))
|
||||
} else {
|
||||
increment = 50;
|
||||
increment = 50
|
||||
}
|
||||
tokenCounterRef.current = Math.min(tokenCounterRef.current + increment, currentResponseLength);
|
||||
tokenCounterRef.current = Math.min(
|
||||
tokenCounterRef.current + increment,
|
||||
currentResponseLength,
|
||||
)
|
||||
}
|
||||
}
|
||||
const displayedResponseLength = tokenCounterRef.current;
|
||||
const leaderTokens = Math.round(displayedResponseLength / 4);
|
||||
const effectiveElapsedMs = hasRunningTeammates ? Math.max(elapsedTimeMs, now - turnStartRef.current) : elapsedTimeMs;
|
||||
const timerText = formatDuration(effectiveElapsedMs);
|
||||
const timerWidth = stringWidth(timerText);
|
||||
const displayedResponseLength = tokenCounterRef.current
|
||||
const leaderTokens = Math.round(displayedResponseLength / 4)
|
||||
|
||||
const effectiveElapsedMs = hasRunningTeammates
|
||||
? Math.max(elapsedTimeMs, now - turnStartRef.current)
|
||||
: elapsedTimeMs
|
||||
const timerText = formatDuration(effectiveElapsedMs)
|
||||
const timerWidth = stringWidth(timerText)
|
||||
|
||||
// === Token count (leader + teammates, or foregrounded teammate) ===
|
||||
const totalTokens = foregroundedTeammate && !foregroundedTeammate.isIdle ? foregroundedTeammate.progress?.tokenCount ?? 0 : leaderTokens + teammateTokens;
|
||||
const tokenCount = formatNumber(totalTokens);
|
||||
const tokensText = hasRunningTeammates ? `${tokenCount} tokens` : `${figures.arrowDown} ${tokenCount} tokens`;
|
||||
const tokensWidth = stringWidth(tokensText);
|
||||
const totalTokens =
|
||||
foregroundedTeammate && !foregroundedTeammate.isIdle
|
||||
? (foregroundedTeammate.progress?.tokenCount ?? 0)
|
||||
: leaderTokens + teammateTokens
|
||||
const tokenCount = formatNumber(totalTokens)
|
||||
const tokensText = hasRunningTeammates
|
||||
? `${tokenCount} tokens`
|
||||
: `${figures.arrowDown} ${tokenCount} tokens`
|
||||
const tokensWidth = stringWidth(tokensText)
|
||||
|
||||
// === Thinking text (may shrink to fit) ===
|
||||
let thinkingText = thinkingStatus === 'thinking' ? `thinking${effortSuffix}` : typeof thinkingStatus === 'number' ? `thought for ${Math.max(1, Math.round(thinkingStatus / 1000))}s` : null;
|
||||
let thinkingWidthValue = thinkingText ? stringWidth(thinkingText) : 0;
|
||||
let thinkingText =
|
||||
thinkingStatus === 'thinking'
|
||||
? `thinking${effortSuffix}`
|
||||
: typeof thinkingStatus === 'number'
|
||||
? `thought for ${Math.max(1, Math.round(thinkingStatus / 1000))}s`
|
||||
: null
|
||||
let thinkingWidthValue = thinkingText ? stringWidth(thinkingText) : 0
|
||||
|
||||
// === Progressive width gating ===
|
||||
const messageWidth = glimmerMessageWidth + 2;
|
||||
const sep = SEP_WIDTH;
|
||||
const wantsThinking = thinkingStatus !== null;
|
||||
const wantsTimerAndTokens = verbose || hasRunningTeammates || effectiveElapsedMs > SHOW_TOKENS_AFTER_MS;
|
||||
const availableSpace = columns - messageWidth - 5;
|
||||
let showThinking = wantsThinking && availableSpace > thinkingWidthValue;
|
||||
if (!showThinking && wantsThinking && thinkingStatus === 'thinking' && effortSuffix) {
|
||||
const messageWidth = glimmerMessageWidth + 2
|
||||
const sep = SEP_WIDTH
|
||||
|
||||
const wantsThinking = thinkingStatus !== null
|
||||
const wantsTimerAndTokens =
|
||||
verbose || hasRunningTeammates || effectiveElapsedMs > SHOW_TOKENS_AFTER_MS
|
||||
|
||||
const availableSpace = columns - messageWidth - 5
|
||||
|
||||
let showThinking = wantsThinking && availableSpace > thinkingWidthValue
|
||||
if (
|
||||
!showThinking &&
|
||||
wantsThinking &&
|
||||
thinkingStatus === 'thinking' &&
|
||||
effortSuffix
|
||||
) {
|
||||
if (availableSpace > THINKING_BARE_WIDTH) {
|
||||
thinkingText = 'thinking';
|
||||
thinkingWidthValue = THINKING_BARE_WIDTH;
|
||||
showThinking = true;
|
||||
thinkingText = 'thinking'
|
||||
thinkingWidthValue = THINKING_BARE_WIDTH
|
||||
showThinking = true
|
||||
}
|
||||
}
|
||||
const usedAfterThinking = showThinking ? thinkingWidthValue + sep : 0;
|
||||
const showTimer = wantsTimerAndTokens && availableSpace > usedAfterThinking + timerWidth;
|
||||
const usedAfterTimer = usedAfterThinking + (showTimer ? timerWidth + sep : 0);
|
||||
const showTokens = wantsTimerAndTokens && totalTokens > 0 && availableSpace > usedAfterTimer + tokensWidth;
|
||||
const thinkingOnly = showThinking && thinkingStatus === 'thinking' && !spinnerSuffix && !showTimer && !showTokens && true;
|
||||
const usedAfterThinking = showThinking ? thinkingWidthValue + sep : 0
|
||||
|
||||
const showTimer =
|
||||
wantsTimerAndTokens && availableSpace > usedAfterThinking + timerWidth
|
||||
const usedAfterTimer = usedAfterThinking + (showTimer ? timerWidth + sep : 0)
|
||||
|
||||
const showTokens =
|
||||
wantsTimerAndTokens &&
|
||||
totalTokens > 0 &&
|
||||
availableSpace > usedAfterTimer + tokensWidth
|
||||
|
||||
|
||||
const thinkingOnly =
|
||||
showThinking &&
|
||||
thinkingStatus === 'thinking' &&
|
||||
!spinnerSuffix &&
|
||||
!showTimer &&
|
||||
!showTokens &&
|
||||
true
|
||||
|
||||
// === Thinking shimmer color (formerly ThinkingShimmerText's own timer) ===
|
||||
// Same sine-wave opacity, but derived from our shared `time` instead of a
|
||||
// second useAnimationFrame(50) subscription.
|
||||
const thinkingElapsedSec = (time - THINKING_DELAY_MS) / 1000;
|
||||
const thinkingOpacity = time < THINKING_DELAY_MS ? 0 : (Math.sin(thinkingElapsedSec * Math.PI * 2 / THINKING_GLOW_PERIOD_S) + 1) / 2;
|
||||
const thinkingShimmerColor = toRGBColor(interpolateColor(THINKING_INACTIVE, THINKING_INACTIVE_SHIMMER, thinkingOpacity));
|
||||
const thinkingElapsedSec = (time - THINKING_DELAY_MS) / 1000
|
||||
const thinkingOpacity =
|
||||
time < THINKING_DELAY_MS
|
||||
? 0
|
||||
: (Math.sin((thinkingElapsedSec * Math.PI * 2) / THINKING_GLOW_PERIOD_S) +
|
||||
1) /
|
||||
2
|
||||
const thinkingShimmerColor = toRGBColor(
|
||||
interpolateColor(
|
||||
THINKING_INACTIVE,
|
||||
THINKING_INACTIVE_SHIMMER,
|
||||
thinkingOpacity,
|
||||
),
|
||||
)
|
||||
|
||||
// === Build status parts ===
|
||||
const parts = [...(spinnerSuffix ? [<Text dimColor key="suffix">
|
||||
const parts = [
|
||||
...(spinnerSuffix
|
||||
? [
|
||||
<Text dimColor key="suffix">
|
||||
{spinnerSuffix}
|
||||
</Text>] : []), ...(showTimer ? [<Text dimColor key="elapsedTime">
|
||||
</Text>,
|
||||
]
|
||||
: []),
|
||||
...(showTimer
|
||||
? [
|
||||
<Text dimColor key="elapsedTime">
|
||||
{timerText}
|
||||
</Text>] : []), ...(showTokens ? [<Box flexDirection="row" key="tokens">
|
||||
</Text>,
|
||||
]
|
||||
: []),
|
||||
...(showTokens
|
||||
? [
|
||||
<Box flexDirection="row" key="tokens">
|
||||
{!hasRunningTeammates && <SpinnerModeGlyph mode={mode} />}
|
||||
<Text dimColor>{tokenCount} tokens</Text>
|
||||
</Box>] : []), ...(showThinking && thinkingText ? [thinkingStatus === 'thinking' && !reducedMotion ? <Text key="thinking" color={thinkingShimmerColor}>
|
||||
</Box>,
|
||||
]
|
||||
: []),
|
||||
...(showThinking && thinkingText
|
||||
? [
|
||||
thinkingStatus === 'thinking' && !reducedMotion ? (
|
||||
<Text key="thinking" color={thinkingShimmerColor}>
|
||||
{thinkingOnly ? `(${thinkingText})` : thinkingText}
|
||||
</Text> : <Text dimColor key="thinking">
|
||||
</Text>
|
||||
) : (
|
||||
<Text dimColor key="thinking">
|
||||
{thinkingText}
|
||||
</Text>] : [])];
|
||||
const status = foregroundedTeammate && !foregroundedTeammate.isIdle ? <>
|
||||
</Text>
|
||||
),
|
||||
]
|
||||
: []),
|
||||
]
|
||||
|
||||
const status =
|
||||
foregroundedTeammate && !foregroundedTeammate.isIdle ? (
|
||||
<>
|
||||
<Text dimColor>(esc to interrupt </Text>
|
||||
<Text color={toInkColor(foregroundedTeammate.identity.color)}>
|
||||
{foregroundedTeammate.identity.agentName}
|
||||
</Text>
|
||||
<Text dimColor>)</Text>
|
||||
</> : !foregroundedTeammate && parts.length > 0 ? thinkingOnly ? <Byline>{parts}</Byline> : <>
|
||||
</>
|
||||
) : !foregroundedTeammate && parts.length > 0 ? (
|
||||
thinkingOnly ? (
|
||||
<Byline>{parts}</Byline>
|
||||
) : (
|
||||
<>
|
||||
<Text dimColor>(</Text>
|
||||
<Byline>{parts}</Byline>
|
||||
<Text dimColor>)</Text>
|
||||
</> : null;
|
||||
return <Box ref={viewportRef} flexDirection="row" flexWrap="wrap" marginTop={1} width="100%">
|
||||
<SpinnerGlyph frame={frame} messageColor={messageColor} stalledIntensity={overrideColor ? 0 : stalledIntensity} reducedMotion={reducedMotion} time={time} />
|
||||
<GlimmerMessage message={message} mode={mode} messageColor={messageColor} glimmerIndex={glimmerIndex} flashOpacity={flashOpacity} shimmerColor={shimmerColor} stalledIntensity={overrideColor ? 0 : stalledIntensity} />
|
||||
</>
|
||||
)
|
||||
) : null
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={viewportRef}
|
||||
flexDirection="row"
|
||||
flexWrap="wrap"
|
||||
marginTop={1}
|
||||
width="100%"
|
||||
>
|
||||
<SpinnerGlyph
|
||||
frame={frame}
|
||||
messageColor={messageColor}
|
||||
stalledIntensity={overrideColor ? 0 : stalledIntensity}
|
||||
reducedMotion={reducedMotion}
|
||||
time={time}
|
||||
/>
|
||||
<GlimmerMessage
|
||||
message={message}
|
||||
mode={mode}
|
||||
messageColor={messageColor}
|
||||
glimmerIndex={glimmerIndex}
|
||||
flashOpacity={flashOpacity}
|
||||
shimmerColor={shimmerColor}
|
||||
stalledIntensity={overrideColor ? 0 : stalledIntensity}
|
||||
/>
|
||||
{status}
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
function SpinnerModeGlyph(t0) {
|
||||
const $ = _c(2);
|
||||
const {
|
||||
mode
|
||||
} = t0;
|
||||
|
||||
function SpinnerModeGlyph({ mode }: { mode: SpinnerMode }): React.ReactNode {
|
||||
switch (mode) {
|
||||
case "tool-input":
|
||||
case "tool-use":
|
||||
case "responding":
|
||||
case "thinking":
|
||||
{
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Box width={2}><Text dimColor={true}>{figures.arrowDown}</Text></Box>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
case "requesting":
|
||||
{
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Box width={2}><Text dimColor={true}>{figures.arrowUp}</Text></Box>;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
return t1;
|
||||
}
|
||||
case 'tool-input':
|
||||
case 'tool-use':
|
||||
case 'responding':
|
||||
case 'thinking':
|
||||
return (
|
||||
<Box width={2}>
|
||||
<Text dimColor>{figures.arrowDown}</Text>
|
||||
</Box>
|
||||
)
|
||||
case 'requesting':
|
||||
return (
|
||||
<Box width={2}>
|
||||
<Text dimColor>{figures.arrowUp}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,79 +1,86 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { Box, Text, useTheme } from '../../ink.js';
|
||||
import { getTheme, type Theme } from '../../utils/theme.js';
|
||||
import { getDefaultCharacters, interpolateColor, parseRGB, toRGBColor } from './utils.js';
|
||||
const DEFAULT_CHARACTERS = getDefaultCharacters();
|
||||
const SPINNER_FRAMES = [...DEFAULT_CHARACTERS, ...[...DEFAULT_CHARACTERS].reverse()];
|
||||
const REDUCED_MOTION_DOT = '●';
|
||||
const REDUCED_MOTION_CYCLE_MS = 2000; // 2-second cycle: 1s visible, 1s dim
|
||||
const ERROR_RED = {
|
||||
r: 171,
|
||||
g: 43,
|
||||
b: 63
|
||||
};
|
||||
import * as React from 'react'
|
||||
import { Box, Text, useTheme } from '../../ink.js'
|
||||
import { getTheme, type Theme } from '../../utils/theme.js'
|
||||
import {
|
||||
getDefaultCharacters,
|
||||
interpolateColor,
|
||||
parseRGB,
|
||||
toRGBColor,
|
||||
} from './utils.js'
|
||||
|
||||
const DEFAULT_CHARACTERS = getDefaultCharacters()
|
||||
|
||||
const SPINNER_FRAMES = [
|
||||
...DEFAULT_CHARACTERS,
|
||||
...[...DEFAULT_CHARACTERS].reverse(),
|
||||
]
|
||||
|
||||
const REDUCED_MOTION_DOT = '●'
|
||||
const REDUCED_MOTION_CYCLE_MS = 2000 // 2-second cycle: 1s visible, 1s dim
|
||||
const ERROR_RED = { r: 171, g: 43, b: 63 }
|
||||
|
||||
type Props = {
|
||||
frame: number;
|
||||
messageColor: keyof Theme;
|
||||
stalledIntensity?: number;
|
||||
reducedMotion?: boolean;
|
||||
time?: number;
|
||||
};
|
||||
export function SpinnerGlyph(t0) {
|
||||
const $ = _c(9);
|
||||
const {
|
||||
frame,
|
||||
messageColor,
|
||||
stalledIntensity: t1,
|
||||
reducedMotion: t2,
|
||||
time: t3
|
||||
} = t0;
|
||||
const stalledIntensity = t1 === undefined ? 0 : t1;
|
||||
const reducedMotion = t2 === undefined ? false : t2;
|
||||
const time = t3 === undefined ? 0 : t3;
|
||||
const [themeName] = useTheme();
|
||||
const theme = getTheme(themeName);
|
||||
if (reducedMotion) {
|
||||
const isDim = Math.floor(time / (REDUCED_MOTION_CYCLE_MS / 2)) % 2 === 1;
|
||||
let t4;
|
||||
if ($[0] !== isDim || $[1] !== messageColor) {
|
||||
t4 = <Box flexWrap="wrap" height={1} width={2}><Text color={messageColor} dimColor={isDim}>{REDUCED_MOTION_DOT}</Text></Box>;
|
||||
$[0] = isDim;
|
||||
$[1] = messageColor;
|
||||
$[2] = t4;
|
||||
} else {
|
||||
t4 = $[2];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
const spinnerChar = SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
|
||||
if (stalledIntensity > 0) {
|
||||
const baseColorStr = theme[messageColor];
|
||||
const baseRGB = baseColorStr ? parseRGB(baseColorStr) : null;
|
||||
if (baseRGB) {
|
||||
const interpolated = interpolateColor(baseRGB, ERROR_RED, stalledIntensity);
|
||||
return <Box flexWrap="wrap" height={1} width={2}><Text color={toRGBColor(interpolated)}>{spinnerChar}</Text></Box>;
|
||||
}
|
||||
const color = stalledIntensity > 0.5 ? "error" : messageColor;
|
||||
let t4;
|
||||
if ($[3] !== color || $[4] !== spinnerChar) {
|
||||
t4 = <Box flexWrap="wrap" height={1} width={2}><Text color={color}>{spinnerChar}</Text></Box>;
|
||||
$[3] = color;
|
||||
$[4] = spinnerChar;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
let t4;
|
||||
if ($[6] !== messageColor || $[7] !== spinnerChar) {
|
||||
t4 = <Box flexWrap="wrap" height={1} width={2}><Text color={messageColor}>{spinnerChar}</Text></Box>;
|
||||
$[6] = messageColor;
|
||||
$[7] = spinnerChar;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
return t4;
|
||||
frame: number
|
||||
messageColor: keyof Theme
|
||||
stalledIntensity?: number
|
||||
reducedMotion?: boolean
|
||||
time?: number
|
||||
}
|
||||
|
||||
export function SpinnerGlyph({
|
||||
frame,
|
||||
messageColor,
|
||||
stalledIntensity = 0,
|
||||
reducedMotion = false,
|
||||
time = 0,
|
||||
}: Props): React.ReactNode {
|
||||
const [themeName] = useTheme()
|
||||
const theme = getTheme(themeName)
|
||||
|
||||
// Reduced motion: slowly flashing orange dot
|
||||
if (reducedMotion) {
|
||||
const isDim = Math.floor(time / (REDUCED_MOTION_CYCLE_MS / 2)) % 2 === 1
|
||||
return (
|
||||
<Box flexWrap="wrap" height={1} width={2}>
|
||||
<Text color={messageColor} dimColor={isDim}>
|
||||
{REDUCED_MOTION_DOT}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const spinnerChar = SPINNER_FRAMES[frame % SPINNER_FRAMES.length]
|
||||
|
||||
// Smoothly interpolate from current color to red when stalled
|
||||
if (stalledIntensity > 0) {
|
||||
const baseColorStr = theme[messageColor]
|
||||
const baseRGB = baseColorStr ? parseRGB(baseColorStr) : null
|
||||
|
||||
if (baseRGB) {
|
||||
const interpolated = interpolateColor(
|
||||
baseRGB,
|
||||
ERROR_RED,
|
||||
stalledIntensity,
|
||||
)
|
||||
return (
|
||||
<Box flexWrap="wrap" height={1} width={2}>
|
||||
<Text color={toRGBColor(interpolated)}>{spinnerChar}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Fallback for ANSI themes
|
||||
const color = stalledIntensity > 0.5 ? 'error' : messageColor
|
||||
return (
|
||||
<Box flexWrap="wrap" height={1} width={2}>
|
||||
<Text color={color}>{spinnerChar}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexWrap="wrap" height={1} width={2}>
|
||||
<Text color={messageColor}>{spinnerChar}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,205 +1,265 @@
|
||||
import figures from 'figures';
|
||||
import sample from 'lodash-es/sample.js';
|
||||
import * as React from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { getSpinnerVerbs } from '../../constants/spinnerVerbs.js';
|
||||
import { TURN_COMPLETION_VERBS } from '../../constants/turnCompletionVerbs.js';
|
||||
import { useElapsedTime } from '../../hooks/useElapsedTime.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import { stringWidth } from '../../ink/stringWidth.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { InProcessTeammateTaskState } from '../../tasks/InProcessTeammateTask/types.js';
|
||||
import { summarizeRecentActivities } from '../../utils/collapseReadSearch.js';
|
||||
import { formatDuration, formatNumber, truncateToWidth } from '../../utils/format.js';
|
||||
import { toInkColor } from '../../utils/ink.js';
|
||||
import { TEAMMATE_SELECT_HINT } from './teammateSelectHint.js';
|
||||
import figures from 'figures'
|
||||
import sample from 'lodash-es/sample.js'
|
||||
import * as React from 'react'
|
||||
import { useRef, useState } from 'react'
|
||||
import { getSpinnerVerbs } from '../../constants/spinnerVerbs.js'
|
||||
import { TURN_COMPLETION_VERBS } from '../../constants/turnCompletionVerbs.js'
|
||||
import { useElapsedTime } from '../../hooks/useElapsedTime.js'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import { stringWidth } from '../../ink/stringWidth.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import type { InProcessTeammateTaskState } from '../../tasks/InProcessTeammateTask/types.js'
|
||||
import { summarizeRecentActivities } from '../../utils/collapseReadSearch.js'
|
||||
import {
|
||||
formatDuration,
|
||||
formatNumber,
|
||||
truncateToWidth,
|
||||
} from '../../utils/format.js'
|
||||
import { toInkColor } from '../../utils/ink.js'
|
||||
import { TEAMMATE_SELECT_HINT } from './teammateSelectHint.js'
|
||||
|
||||
type Props = {
|
||||
teammate: InProcessTeammateTaskState;
|
||||
isLast: boolean;
|
||||
isSelected?: boolean;
|
||||
isForegrounded?: boolean;
|
||||
allIdle?: boolean;
|
||||
showPreview?: boolean;
|
||||
};
|
||||
teammate: InProcessTeammateTaskState
|
||||
isLast: boolean
|
||||
isSelected?: boolean
|
||||
isForegrounded?: boolean
|
||||
allIdle?: boolean
|
||||
showPreview?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the last 3 lines of content from a teammate's conversation.
|
||||
* Shows recent activity from any message type (user or assistant).
|
||||
*/
|
||||
function getMessagePreview(messages: InProcessTeammateTaskState['messages']): string[] {
|
||||
if (!messages?.length) return [];
|
||||
const allLines: string[] = [];
|
||||
const maxLineLength = 80;
|
||||
function getMessagePreview(
|
||||
messages: InProcessTeammateTaskState['messages'],
|
||||
): string[] {
|
||||
if (!messages?.length) return []
|
||||
|
||||
const allLines: string[] = []
|
||||
const maxLineLength = 80
|
||||
|
||||
// Collect lines from recent messages (newest first)
|
||||
for (let i = messages.length - 1; i >= 0 && allLines.length < 3; i--) {
|
||||
const msg = messages[i];
|
||||
const msg = messages[i]
|
||||
// Only process messages that have content (user/assistant messages)
|
||||
if (!msg || msg.type !== 'user' && msg.type !== 'assistant' || !msg.message?.content?.length) {
|
||||
continue;
|
||||
if (
|
||||
!msg ||
|
||||
(msg.type !== 'user' && msg.type !== 'assistant') ||
|
||||
!msg.message?.content?.length
|
||||
) {
|
||||
continue
|
||||
}
|
||||
const content = msg.message.content;
|
||||
const content = msg.message.content
|
||||
|
||||
for (const block of content) {
|
||||
if (allLines.length >= 3) break;
|
||||
if (!block || typeof block !== 'object') continue;
|
||||
if (allLines.length >= 3) break
|
||||
if (!block || typeof block !== 'object') continue
|
||||
|
||||
if ('type' in block && block.type === 'tool_use' && 'name' in block) {
|
||||
// Try to show meaningful info from tool input
|
||||
const input = 'input' in block ? block.input as Record<string, unknown> : null;
|
||||
let toolLine = `Using ${block.name}…`;
|
||||
const input =
|
||||
'input' in block ? (block.input as Record<string, unknown>) : null
|
||||
let toolLine = `Using ${block.name}…`
|
||||
if (input) {
|
||||
// Look for common descriptive fields
|
||||
const desc = input.description as string | undefined || input.prompt as string | undefined || input.command as string | undefined || input.query as string | undefined || input.pattern as string | undefined;
|
||||
const desc =
|
||||
(input.description as string | undefined) ||
|
||||
(input.prompt as string | undefined) ||
|
||||
(input.command as string | undefined) ||
|
||||
(input.query as string | undefined) ||
|
||||
(input.pattern as string | undefined)
|
||||
if (desc) {
|
||||
toolLine = desc.split('\n')[0] ?? toolLine;
|
||||
toolLine = desc.split('\n')[0] ?? toolLine
|
||||
}
|
||||
}
|
||||
allLines.push(truncateToWidth(toolLine, maxLineLength));
|
||||
allLines.push(truncateToWidth(toolLine, maxLineLength))
|
||||
} else if ('type' in block && block.type === 'text' && 'text' in block) {
|
||||
const textLines = (block.text as string).split('\n').filter(l => l.trim());
|
||||
const textLines = (block.text as string)
|
||||
.split('\n')
|
||||
.filter(l => l.trim())
|
||||
// Take from end of text (most recent lines)
|
||||
for (let j = textLines.length - 1; j >= 0 && allLines.length < 3; j--) {
|
||||
const line = textLines[j];
|
||||
if (!line) continue;
|
||||
allLines.push(truncateToWidth(line, maxLineLength));
|
||||
const line = textLines[j]
|
||||
if (!line) continue
|
||||
allLines.push(truncateToWidth(line, maxLineLength))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse so oldest of the 3 is first (reading order)
|
||||
return allLines.reverse();
|
||||
return allLines.reverse()
|
||||
}
|
||||
|
||||
export function TeammateSpinnerLine({
|
||||
teammate,
|
||||
isLast,
|
||||
isSelected,
|
||||
isForegrounded,
|
||||
allIdle,
|
||||
showPreview
|
||||
showPreview,
|
||||
}: Props): React.ReactNode {
|
||||
const [randomVerb] = useState(() => teammate.spinnerVerb ?? sample(getSpinnerVerbs()));
|
||||
const [pastTenseVerb] = useState(() => teammate.pastTenseVerb ?? sample(TURN_COMPLETION_VERBS));
|
||||
const isHighlighted = isSelected || isForegrounded;
|
||||
const treeChar = isHighlighted ? isLast ? '╘═' : '╞═' : isLast ? '└─' : '├─';
|
||||
const nameColor = toInkColor(teammate.identity.color);
|
||||
const {
|
||||
columns
|
||||
} = useTerminalSize();
|
||||
const [randomVerb] = useState(
|
||||
() => teammate.spinnerVerb ?? sample(getSpinnerVerbs()),
|
||||
)
|
||||
const [pastTenseVerb] = useState(
|
||||
() => teammate.pastTenseVerb ?? sample(TURN_COMPLETION_VERBS),
|
||||
)
|
||||
const isHighlighted = isSelected || isForegrounded
|
||||
const treeChar = isHighlighted ? (isLast ? '╘═' : '╞═') : isLast ? '└─' : '├─'
|
||||
const nameColor = toInkColor(teammate.identity.color)
|
||||
const { columns } = useTerminalSize()
|
||||
|
||||
// Track when teammate became idle (for "Idle for X..." display)
|
||||
const idleStartRef = useRef<number | null>(null);
|
||||
const idleStartRef = useRef<number | null>(null)
|
||||
// Freeze elapsed time when entering all-idle state
|
||||
const frozenDurationRef = useRef<string | null>(null);
|
||||
const frozenDurationRef = useRef<string | null>(null)
|
||||
|
||||
// Track idle start time
|
||||
if (teammate.isIdle && idleStartRef.current === null) {
|
||||
idleStartRef.current = Date.now();
|
||||
idleStartRef.current = Date.now()
|
||||
} else if (!teammate.isIdle) {
|
||||
idleStartRef.current = null;
|
||||
idleStartRef.current = null
|
||||
}
|
||||
|
||||
// Reset frozen duration when leaving all-idle state
|
||||
if (!allIdle && frozenDurationRef.current !== null) {
|
||||
frozenDurationRef.current = null;
|
||||
frozenDurationRef.current = null
|
||||
}
|
||||
|
||||
// Get elapsed idle time (how long they've been idle) - for "Idle for X..." display
|
||||
const idleElapsedTime = useElapsedTime(idleStartRef.current ?? Date.now(), teammate.isIdle && !allIdle);
|
||||
const idleElapsedTime = useElapsedTime(
|
||||
idleStartRef.current ?? Date.now(),
|
||||
teammate.isIdle && !allIdle,
|
||||
)
|
||||
|
||||
// Freeze the duration when we first detect all idle
|
||||
// Use the teammate's actual work time (since task started) for the past-tense display
|
||||
if (allIdle && frozenDurationRef.current === null) {
|
||||
frozenDurationRef.current = formatDuration(Math.max(0, Date.now() - teammate.startTime - (teammate.totalPausedMs ?? 0)));
|
||||
frozenDurationRef.current = formatDuration(
|
||||
Math.max(
|
||||
0,
|
||||
Date.now() - teammate.startTime - (teammate.totalPausedMs ?? 0),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Use frozen work duration when all idle, otherwise use idle elapsed time
|
||||
const displayTime = allIdle ? frozenDurationRef.current ?? (() => {
|
||||
throw new Error(`frozenDurationRef is null for idle teammate ${teammate.identity.agentName}`);
|
||||
})() : idleElapsedTime;
|
||||
const displayTime = allIdle
|
||||
? (frozenDurationRef.current ??
|
||||
(() => {
|
||||
throw new Error(
|
||||
`frozenDurationRef is null for idle teammate ${teammate.identity.agentName}`,
|
||||
)
|
||||
})())
|
||||
: idleElapsedTime
|
||||
|
||||
// Layout: paddingLeft(3) + pointer(1) + space(1) + treeChar(2) + space(1) = 8 fixed chars
|
||||
// Then optionally: @name + ": " OR just ": "
|
||||
// Then: activity text + optional extras (stats, hints)
|
||||
const basePrefix = 8;
|
||||
const fullAgentName = `@${teammate.identity.agentName}`;
|
||||
const fullNameWidth = stringWidth(fullAgentName);
|
||||
const basePrefix = 8
|
||||
const fullAgentName = `@${teammate.identity.agentName}`
|
||||
const fullNameWidth = stringWidth(fullAgentName)
|
||||
|
||||
// Get stats from progress
|
||||
const toolUseCount = teammate.progress?.toolUseCount ?? 0;
|
||||
const tokenCount = teammate.progress?.tokenCount ?? 0;
|
||||
const statsText = ` · ${toolUseCount} tool ${toolUseCount === 1 ? 'use' : 'uses'} · ${formatNumber(tokenCount)} tokens`;
|
||||
const statsWidth = stringWidth(statsText);
|
||||
const selectHintText = ` · ${TEAMMATE_SELECT_HINT}`;
|
||||
const selectHintWidth = stringWidth(selectHintText);
|
||||
const viewHintText = ' · enter to view';
|
||||
const viewHintWidth = stringWidth(viewHintText);
|
||||
const toolUseCount = teammate.progress?.toolUseCount ?? 0
|
||||
const tokenCount = teammate.progress?.tokenCount ?? 0
|
||||
const statsText = ` · ${toolUseCount} tool ${toolUseCount === 1 ? 'use' : 'uses'} · ${formatNumber(tokenCount)} tokens`
|
||||
const statsWidth = stringWidth(statsText)
|
||||
const selectHintText = ` · ${TEAMMATE_SELECT_HINT}`
|
||||
const selectHintWidth = stringWidth(selectHintText)
|
||||
const viewHintText = ' · enter to view'
|
||||
const viewHintWidth = stringWidth(viewHintText)
|
||||
|
||||
// Progressive responsive layout:
|
||||
// Wide (80+): full name + activity + stats + hint
|
||||
// Medium (60-80): full name + activity
|
||||
// Narrow (<60): hide name, just show activity
|
||||
const minActivityWidth = 25;
|
||||
const minActivityWidth = 25
|
||||
|
||||
// Hide name on narrow terminals (< 60 cols) or if there's not enough room
|
||||
const spaceWithFullName = columns - basePrefix - fullNameWidth - 2;
|
||||
const showName = columns >= 60 && spaceWithFullName >= minActivityWidth;
|
||||
const nameWidth = showName ? fullNameWidth + 2 : 0; // +2 for ": " when name shown
|
||||
const availableForActivity = columns - basePrefix - nameWidth;
|
||||
const spaceWithFullName = columns - basePrefix - fullNameWidth - 2
|
||||
const showName = columns >= 60 && spaceWithFullName >= minActivityWidth
|
||||
const nameWidth = showName ? fullNameWidth + 2 : 0 // +2 for ": " when name shown
|
||||
const availableForActivity = columns - basePrefix - nameWidth
|
||||
|
||||
// Progressive hiding: view hint → select hint → stats
|
||||
// Stats always visible (dimmed when not selected); hints only when highlighted/selected
|
||||
const showViewHint = isSelected && !isForegrounded && availableForActivity > viewHintWidth + statsWidth + minActivityWidth + 5;
|
||||
const showSelectHint = isHighlighted && availableForActivity > selectHintWidth + (showViewHint ? viewHintWidth : 0) + statsWidth + minActivityWidth + 5;
|
||||
const showStats = availableForActivity > statsWidth + minActivityWidth + 5;
|
||||
const showViewHint =
|
||||
isSelected &&
|
||||
!isForegrounded &&
|
||||
availableForActivity > viewHintWidth + statsWidth + minActivityWidth + 5
|
||||
const showSelectHint =
|
||||
isHighlighted &&
|
||||
availableForActivity >
|
||||
selectHintWidth +
|
||||
(showViewHint ? viewHintWidth : 0) +
|
||||
statsWidth +
|
||||
minActivityWidth +
|
||||
5
|
||||
const showStats = availableForActivity > statsWidth + minActivityWidth + 5
|
||||
|
||||
// Activity text gets remaining space
|
||||
const extrasCost = (showStats ? statsWidth : 0) + (showSelectHint ? selectHintWidth : 0) + (showViewHint ? viewHintWidth : 0);
|
||||
const activityMaxWidth = Math.max(minActivityWidth, availableForActivity - extrasCost - 1);
|
||||
const extrasCost =
|
||||
(showStats ? statsWidth : 0) +
|
||||
(showSelectHint ? selectHintWidth : 0) +
|
||||
(showViewHint ? viewHintWidth : 0)
|
||||
const activityMaxWidth = Math.max(
|
||||
minActivityWidth,
|
||||
availableForActivity - extrasCost - 1,
|
||||
)
|
||||
|
||||
// Format the activity text for active teammates, rolling up search/read ops
|
||||
const activityText = (() => {
|
||||
const activities = teammate.progress?.recentActivities;
|
||||
const activities = teammate.progress?.recentActivities
|
||||
if (activities && activities.length > 0) {
|
||||
const summary = summarizeRecentActivities(activities);
|
||||
if (summary) return truncateToWidth(summary, activityMaxWidth);
|
||||
const summary = summarizeRecentActivities(activities)
|
||||
if (summary) return truncateToWidth(summary, activityMaxWidth)
|
||||
}
|
||||
const desc = teammate.progress?.lastActivity?.activityDescription;
|
||||
if (desc) return truncateToWidth(desc, activityMaxWidth);
|
||||
return randomVerb;
|
||||
})();
|
||||
const desc = teammate.progress?.lastActivity?.activityDescription
|
||||
if (desc) return truncateToWidth(desc, activityMaxWidth)
|
||||
return randomVerb
|
||||
})()
|
||||
|
||||
// Status rendering logic
|
||||
const renderStatus = (): React.ReactNode => {
|
||||
if (teammate.shutdownRequested) {
|
||||
return <Text dimColor>[stopping]</Text>;
|
||||
return <Text dimColor>[stopping]</Text>
|
||||
}
|
||||
if (teammate.awaitingPlanApproval) {
|
||||
return <Text color="warning">[awaiting approval]</Text>;
|
||||
return <Text color="warning">[awaiting approval]</Text>
|
||||
}
|
||||
if (teammate.isIdle) {
|
||||
if (allIdle) {
|
||||
return <Text dimColor>
|
||||
return (
|
||||
<Text dimColor>
|
||||
{pastTenseVerb} for {displayTime}
|
||||
</Text>;
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
return <Text dimColor>Idle for {idleElapsedTime}</Text>;
|
||||
return <Text dimColor>Idle for {idleElapsedTime}</Text>
|
||||
}
|
||||
// Active - show spinner glyph + activity description (only when not highlighted;
|
||||
// when highlighted, the main spinner above already shows the verb)
|
||||
if (isHighlighted) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
return <Text dimColor>
|
||||
return (
|
||||
<Text dimColor>
|
||||
{activityText?.endsWith('…') ? activityText : `${activityText}…`}
|
||||
</Text>;
|
||||
};
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
// Get preview lines if enabled
|
||||
const previewLines = showPreview ? getMessagePreview(teammate.messages) : [];
|
||||
const previewLines = showPreview ? getMessagePreview(teammate.messages) : []
|
||||
|
||||
// Tree continuation character for preview lines
|
||||
const previewTreeChar = isLast ? ' ' : '│ ';
|
||||
return <Box flexDirection="column">
|
||||
const previewTreeChar = isLast ? ' ' : '│ '
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box paddingLeft={3}>
|
||||
{/* Selection indicator: pointer when selected, otherwise space */}
|
||||
<Text color={isSelected ? 'suggestion' : undefined} bold={isSelected}>
|
||||
@@ -207,26 +267,33 @@ export function TeammateSpinnerLine({
|
||||
</Text>
|
||||
<Text dimColor={!isSelected}>{treeChar} </Text>
|
||||
{/* Agent name: hidden on very narrow screens */}
|
||||
{showName && <Text color={isSelected ? 'suggestion' : nameColor}>
|
||||
{showName && (
|
||||
<Text color={isSelected ? 'suggestion' : nameColor}>
|
||||
@{teammate.identity.agentName}
|
||||
</Text>}
|
||||
</Text>
|
||||
)}
|
||||
{showName && <Text dimColor={!isSelected}>: </Text>}
|
||||
{renderStatus()}
|
||||
{/* Stats: only shown when selected and terminal is wide enough */}
|
||||
{showStats && <Text dimColor>
|
||||
{showStats && (
|
||||
<Text dimColor>
|
||||
{' '}
|
||||
· {toolUseCount} tool {toolUseCount === 1 ? 'use' : 'uses'} ·{' '}
|
||||
{formatNumber(tokenCount)} tokens
|
||||
</Text>}
|
||||
</Text>
|
||||
)}
|
||||
{/* Hints: select hint when highlighted, view hint when selected but not foregrounded */}
|
||||
{showSelectHint && <Text dimColor> · {TEAMMATE_SELECT_HINT}</Text>}
|
||||
{showViewHint && <Text dimColor> · enter to view</Text>}
|
||||
</Box>
|
||||
{/* Preview lines */}
|
||||
{previewLines.map((line, idx) => <Box key={idx} paddingLeft={3}>
|
||||
{previewLines.map((line, idx) => (
|
||||
<Box key={idx} paddingLeft={3}>
|
||||
<Text dimColor> </Text>
|
||||
<Text dimColor>{previewTreeChar} </Text>
|
||||
<Text dimColor>{line}</Text>
|
||||
</Box>)}
|
||||
</Box>;
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,271 +1,130 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { Box, Text, type TextProps } from '../../ink.js';
|
||||
import { useAppState } from '../../state/AppState.js';
|
||||
import { getRunningTeammatesSorted } from '../../tasks/InProcessTeammateTask/InProcessTeammateTask.js';
|
||||
import { formatNumber } from '../../utils/format.js';
|
||||
import { TeammateSpinnerLine } from './TeammateSpinnerLine.js';
|
||||
import { TEAMMATE_SELECT_HINT } from './teammateSelectHint.js';
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { Box, Text, type TextProps } from '../../ink.js'
|
||||
import { useAppState } from '../../state/AppState.js'
|
||||
import { getRunningTeammatesSorted } from '../../tasks/InProcessTeammateTask/InProcessTeammateTask.js'
|
||||
import { formatNumber } from '../../utils/format.js'
|
||||
import { TeammateSpinnerLine } from './TeammateSpinnerLine.js'
|
||||
import { TEAMMATE_SELECT_HINT } from './teammateSelectHint.js'
|
||||
|
||||
type Props = {
|
||||
selectedIndex?: number;
|
||||
isInSelectionMode?: boolean;
|
||||
allIdle?: boolean;
|
||||
selectedIndex?: number
|
||||
isInSelectionMode?: boolean
|
||||
allIdle?: boolean
|
||||
/** Leader's active verb (when leader is actively processing) */
|
||||
leaderVerb?: string;
|
||||
leaderVerb?: string
|
||||
/** Leader's token count (when leader is actively processing) */
|
||||
leaderTokenCount?: number;
|
||||
leaderTokenCount?: number
|
||||
/** Leader's idle status text (when leader is idle, e.g. "✻ Idle for 3s") */
|
||||
leaderIdleText?: string;
|
||||
};
|
||||
export function TeammateSpinnerTree(t0) {
|
||||
const $ = _c(61);
|
||||
const {
|
||||
selectedIndex,
|
||||
isInSelectionMode,
|
||||
allIdle,
|
||||
leaderVerb,
|
||||
leaderTokenCount,
|
||||
leaderIdleText
|
||||
} = t0;
|
||||
const tasks = useAppState(_temp);
|
||||
const viewingAgentTaskId = useAppState(_temp2);
|
||||
const showTeammateMessagePreview = useAppState(_temp3);
|
||||
let T0;
|
||||
let isHideSelected;
|
||||
let t1;
|
||||
let t2;
|
||||
let t3;
|
||||
let t4;
|
||||
let t5;
|
||||
if ($[0] !== allIdle || $[1] !== isInSelectionMode || $[2] !== leaderIdleText || $[3] !== leaderTokenCount || $[4] !== leaderVerb || $[5] !== selectedIndex || $[6] !== showTeammateMessagePreview || $[7] !== tasks || $[8] !== viewingAgentTaskId) {
|
||||
t5 = Symbol.for("react.early_return_sentinel");
|
||||
bb0: {
|
||||
const teammateTasks = getRunningTeammatesSorted(tasks);
|
||||
if (teammateTasks.length === 0) {
|
||||
t5 = null;
|
||||
break bb0;
|
||||
}
|
||||
const isLeaderForegrounded = viewingAgentTaskId === undefined;
|
||||
const isLeaderSelected = isInSelectionMode && selectedIndex === -1;
|
||||
const isLeaderHighlighted = isLeaderForegrounded || isLeaderSelected;
|
||||
isHideSelected = isInSelectionMode === true && selectedIndex === teammateTasks.length;
|
||||
T0 = Box;
|
||||
t1 = "column";
|
||||
t2 = 1;
|
||||
const t6 = isLeaderSelected ? "suggestion" : undefined;
|
||||
const t7 = isLeaderSelected ? figures.pointer : " ";
|
||||
let t8;
|
||||
if ($[16] !== isLeaderHighlighted || $[17] !== t6 || $[18] !== t7) {
|
||||
t8 = <Text color={t6} bold={isLeaderHighlighted}>{t7}</Text>;
|
||||
$[16] = isLeaderHighlighted;
|
||||
$[17] = t6;
|
||||
$[18] = t7;
|
||||
$[19] = t8;
|
||||
} else {
|
||||
t8 = $[19];
|
||||
}
|
||||
const t9 = !isLeaderHighlighted;
|
||||
const t10 = isLeaderHighlighted ? "\u2552\u2550" : "\u250C\u2500";
|
||||
let t11;
|
||||
if ($[20] !== isLeaderHighlighted || $[21] !== t10 || $[22] !== t9) {
|
||||
t11 = <Text dimColor={t9} bold={isLeaderHighlighted}>{t10}{" "}</Text>;
|
||||
$[20] = isLeaderHighlighted;
|
||||
$[21] = t10;
|
||||
$[22] = t9;
|
||||
$[23] = t11;
|
||||
} else {
|
||||
t11 = $[23];
|
||||
}
|
||||
const t12 = isLeaderSelected ? "suggestion" : "cyan_FOR_SUBAGENTS_ONLY";
|
||||
let t13;
|
||||
if ($[24] !== isLeaderHighlighted || $[25] !== t12) {
|
||||
t13 = <Text bold={isLeaderHighlighted} color={t12}>team-lead</Text>;
|
||||
$[24] = isLeaderHighlighted;
|
||||
$[25] = t12;
|
||||
$[26] = t13;
|
||||
} else {
|
||||
t13 = $[26];
|
||||
}
|
||||
let t14;
|
||||
if ($[27] !== isLeaderForegrounded || $[28] !== leaderVerb) {
|
||||
t14 = !isLeaderForegrounded && leaderVerb && <Text dimColor={true}>: {leaderVerb}…</Text>;
|
||||
$[27] = isLeaderForegrounded;
|
||||
$[28] = leaderVerb;
|
||||
$[29] = t14;
|
||||
} else {
|
||||
t14 = $[29];
|
||||
}
|
||||
let t15;
|
||||
if ($[30] !== isLeaderForegrounded || $[31] !== leaderIdleText || $[32] !== leaderVerb) {
|
||||
t15 = !isLeaderForegrounded && !leaderVerb && leaderIdleText && <Text dimColor={true}>: {leaderIdleText}</Text>;
|
||||
$[30] = isLeaderForegrounded;
|
||||
$[31] = leaderIdleText;
|
||||
$[32] = leaderVerb;
|
||||
$[33] = t15;
|
||||
} else {
|
||||
t15 = $[33];
|
||||
}
|
||||
let t16;
|
||||
if ($[34] !== isLeaderHighlighted || $[35] !== leaderTokenCount) {
|
||||
t16 = leaderTokenCount !== undefined && leaderTokenCount > 0 && <Text dimColor={!isLeaderHighlighted}>{" "}· {formatNumber(leaderTokenCount)} tokens</Text>;
|
||||
$[34] = isLeaderHighlighted;
|
||||
$[35] = leaderTokenCount;
|
||||
$[36] = t16;
|
||||
} else {
|
||||
t16 = $[36];
|
||||
}
|
||||
let t17;
|
||||
if ($[37] !== isLeaderHighlighted) {
|
||||
t17 = isLeaderHighlighted && <Text dimColor={true}> · {TEAMMATE_SELECT_HINT}</Text>;
|
||||
$[37] = isLeaderHighlighted;
|
||||
$[38] = t17;
|
||||
} else {
|
||||
t17 = $[38];
|
||||
}
|
||||
let t18;
|
||||
if ($[39] !== isLeaderForegrounded || $[40] !== isLeaderSelected) {
|
||||
t18 = isLeaderSelected && !isLeaderForegrounded && <Text dimColor={true}> · enter to view</Text>;
|
||||
$[39] = isLeaderForegrounded;
|
||||
$[40] = isLeaderSelected;
|
||||
$[41] = t18;
|
||||
} else {
|
||||
t18 = $[41];
|
||||
}
|
||||
if ($[42] !== t11 || $[43] !== t13 || $[44] !== t14 || $[45] !== t15 || $[46] !== t16 || $[47] !== t17 || $[48] !== t18 || $[49] !== t8) {
|
||||
t3 = <Box paddingLeft={3}>{t8}{t11}{t13}{t14}{t15}{t16}{t17}{t18}</Box>;
|
||||
$[42] = t11;
|
||||
$[43] = t13;
|
||||
$[44] = t14;
|
||||
$[45] = t15;
|
||||
$[46] = t16;
|
||||
$[47] = t17;
|
||||
$[48] = t18;
|
||||
$[49] = t8;
|
||||
$[50] = t3;
|
||||
} else {
|
||||
t3 = $[50];
|
||||
}
|
||||
t4 = teammateTasks.map((teammate, index) => <TeammateSpinnerLine key={teammate.id} teammate={teammate} isLast={!isInSelectionMode && index === teammateTasks.length - 1} isSelected={isInSelectionMode && selectedIndex === index} isForegrounded={viewingAgentTaskId === teammate.id} allIdle={allIdle} showPreview={showTeammateMessagePreview} />);
|
||||
}
|
||||
$[0] = allIdle;
|
||||
$[1] = isInSelectionMode;
|
||||
$[2] = leaderIdleText;
|
||||
$[3] = leaderTokenCount;
|
||||
$[4] = leaderVerb;
|
||||
$[5] = selectedIndex;
|
||||
$[6] = showTeammateMessagePreview;
|
||||
$[7] = tasks;
|
||||
$[8] = viewingAgentTaskId;
|
||||
$[9] = T0;
|
||||
$[10] = isHideSelected;
|
||||
$[11] = t1;
|
||||
$[12] = t2;
|
||||
$[13] = t3;
|
||||
$[14] = t4;
|
||||
$[15] = t5;
|
||||
} else {
|
||||
T0 = $[9];
|
||||
isHideSelected = $[10];
|
||||
t1 = $[11];
|
||||
t2 = $[12];
|
||||
t3 = $[13];
|
||||
t4 = $[14];
|
||||
t5 = $[15];
|
||||
}
|
||||
if (t5 !== Symbol.for("react.early_return_sentinel")) {
|
||||
return t5;
|
||||
}
|
||||
let t6;
|
||||
if ($[51] !== isHideSelected || $[52] !== isInSelectionMode) {
|
||||
t6 = isInSelectionMode && <HideRow isSelected={isHideSelected} />;
|
||||
$[51] = isHideSelected;
|
||||
$[52] = isInSelectionMode;
|
||||
$[53] = t6;
|
||||
} else {
|
||||
t6 = $[53];
|
||||
}
|
||||
let t7;
|
||||
if ($[54] !== T0 || $[55] !== t1 || $[56] !== t2 || $[57] !== t3 || $[58] !== t4 || $[59] !== t6) {
|
||||
t7 = <T0 flexDirection={t1} marginTop={t2}>{t3}{t4}{t6}</T0>;
|
||||
$[54] = T0;
|
||||
$[55] = t1;
|
||||
$[56] = t2;
|
||||
$[57] = t3;
|
||||
$[58] = t4;
|
||||
$[59] = t6;
|
||||
$[60] = t7;
|
||||
} else {
|
||||
t7 = $[60];
|
||||
}
|
||||
return t7;
|
||||
leaderIdleText?: string
|
||||
}
|
||||
function _temp3(s_1) {
|
||||
return s_1.showTeammateMessagePreview;
|
||||
|
||||
export function TeammateSpinnerTree({
|
||||
selectedIndex,
|
||||
isInSelectionMode,
|
||||
allIdle,
|
||||
leaderVerb,
|
||||
leaderTokenCount,
|
||||
leaderIdleText,
|
||||
}: Props): React.ReactNode {
|
||||
const tasks = useAppState(s => s.tasks)
|
||||
const viewingAgentTaskId = useAppState(s => s.viewingAgentTaskId)
|
||||
const showTeammateMessagePreview = useAppState(
|
||||
s => s.showTeammateMessagePreview,
|
||||
)
|
||||
|
||||
const teammateTasks = getRunningTeammatesSorted(tasks)
|
||||
|
||||
// Don't render if no running teammates
|
||||
if (teammateTasks.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Leader highlighting follows same pattern as teammates:
|
||||
// isHighlighted = isForegrounded || isSelected
|
||||
const isLeaderForegrounded = viewingAgentTaskId === undefined
|
||||
const isLeaderSelected = isInSelectionMode && selectedIndex === -1
|
||||
const isLeaderHighlighted = isLeaderForegrounded || isLeaderSelected
|
||||
const leaderColor: TextProps['color'] = 'cyan_FOR_SUBAGENTS_ONLY'
|
||||
|
||||
// Is the "hide" row selected? (index === teammateCount in selection mode)
|
||||
const isHideSelected =
|
||||
isInSelectionMode === true && selectedIndex === teammateTasks.length
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
{/* Leader row - always visible, uses ┌─ to enclose the tree */}
|
||||
{
|
||||
<Box paddingLeft={3}>
|
||||
<Text
|
||||
color={isLeaderSelected ? 'suggestion' : undefined}
|
||||
bold={isLeaderHighlighted}
|
||||
>
|
||||
{isLeaderSelected ? figures.pointer : ' '}
|
||||
</Text>
|
||||
<Text dimColor={!isLeaderHighlighted} bold={isLeaderHighlighted}>
|
||||
{isLeaderHighlighted ? '╒═' : '┌─'}{' '}
|
||||
</Text>
|
||||
<Text
|
||||
bold={isLeaderHighlighted}
|
||||
color={isLeaderSelected ? 'suggestion' : leaderColor}
|
||||
>
|
||||
team-lead
|
||||
</Text>
|
||||
{/* When backgrounded and active: show spinner + verb */}
|
||||
{!isLeaderForegrounded && leaderVerb && (
|
||||
<Text dimColor>: {leaderVerb}…</Text>
|
||||
)}
|
||||
{/* When backgrounded and idle: show idle text */}
|
||||
{!isLeaderForegrounded && !leaderVerb && leaderIdleText && (
|
||||
<Text dimColor>: {leaderIdleText}</Text>
|
||||
)}
|
||||
{/* Stats (tokens) - same dimColor logic as teammates */}
|
||||
{leaderTokenCount !== undefined && leaderTokenCount > 0 && (
|
||||
<Text dimColor={!isLeaderHighlighted}>
|
||||
{' '}
|
||||
· {formatNumber(leaderTokenCount)} tokens
|
||||
</Text>
|
||||
)}
|
||||
{/* Hints - select hint when highlighted, view hint when selected but not foregrounded */}
|
||||
{isLeaderHighlighted && (
|
||||
<Text dimColor> · {TEAMMATE_SELECT_HINT}</Text>
|
||||
)}
|
||||
{isLeaderSelected && !isLeaderForegrounded && (
|
||||
<Text dimColor> · enter to view</Text>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
{teammateTasks.map((teammate, index) => (
|
||||
<TeammateSpinnerLine
|
||||
key={teammate.id}
|
||||
teammate={teammate}
|
||||
isLast={!isInSelectionMode && index === teammateTasks.length - 1}
|
||||
isSelected={isInSelectionMode && selectedIndex === index}
|
||||
isForegrounded={viewingAgentTaskId === teammate.id}
|
||||
allIdle={allIdle}
|
||||
showPreview={showTeammateMessagePreview}
|
||||
/>
|
||||
))}
|
||||
{/* Hide row - only visible during selection mode */}
|
||||
{isInSelectionMode && <HideRow isSelected={isHideSelected} />}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
function _temp2(s_0) {
|
||||
return s_0.viewingAgentTaskId;
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.tasks;
|
||||
}
|
||||
function HideRow(t0) {
|
||||
const $ = _c(18);
|
||||
const {
|
||||
isSelected
|
||||
} = t0;
|
||||
const t1 = isSelected ? "suggestion" : undefined;
|
||||
const t2 = isSelected ? figures.pointer : " ";
|
||||
let t3;
|
||||
if ($[0] !== isSelected || $[1] !== t1 || $[2] !== t2) {
|
||||
t3 = <Text color={t1} bold={isSelected}>{t2}</Text>;
|
||||
$[0] = isSelected;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
const t4 = !isSelected;
|
||||
const t5 = isSelected ? "\u2558\u2550" : "\u2514\u2500";
|
||||
let t6;
|
||||
if ($[4] !== isSelected || $[5] !== t4 || $[6] !== t5) {
|
||||
t6 = <Text dimColor={t4} bold={isSelected}>{t5}{" "}</Text>;
|
||||
$[4] = isSelected;
|
||||
$[5] = t4;
|
||||
$[6] = t5;
|
||||
$[7] = t6;
|
||||
} else {
|
||||
t6 = $[7];
|
||||
}
|
||||
const t7 = !isSelected;
|
||||
let t8;
|
||||
if ($[8] !== isSelected || $[9] !== t7) {
|
||||
t8 = <Text dimColor={t7} bold={isSelected}>hide</Text>;
|
||||
$[8] = isSelected;
|
||||
$[9] = t7;
|
||||
$[10] = t8;
|
||||
} else {
|
||||
t8 = $[10];
|
||||
}
|
||||
let t9;
|
||||
if ($[11] !== isSelected) {
|
||||
t9 = isSelected && <Text dimColor={true}> · enter to collapse</Text>;
|
||||
$[11] = isSelected;
|
||||
$[12] = t9;
|
||||
} else {
|
||||
t9 = $[12];
|
||||
}
|
||||
let t10;
|
||||
if ($[13] !== t3 || $[14] !== t6 || $[15] !== t8 || $[16] !== t9) {
|
||||
t10 = <Box paddingLeft={3}>{t3}{t6}{t8}{t9}</Box>;
|
||||
$[13] = t3;
|
||||
$[14] = t6;
|
||||
$[15] = t8;
|
||||
$[16] = t9;
|
||||
$[17] = t10;
|
||||
} else {
|
||||
t10 = $[17];
|
||||
}
|
||||
return t10;
|
||||
|
||||
function HideRow({ isSelected }: { isSelected: boolean }): React.ReactNode {
|
||||
return (
|
||||
<Box paddingLeft={3}>
|
||||
<Text color={isSelected ? 'suggestion' : undefined} bold={isSelected}>
|
||||
{isSelected ? figures.pointer : ' '}
|
||||
</Text>
|
||||
<Text dimColor={!isSelected} bold={isSelected}>
|
||||
{isSelected ? '╘═' : '└─'}{' '}
|
||||
</Text>
|
||||
<Text dimColor={!isSelected} bold={isSelected}>
|
||||
hide
|
||||
</Text>
|
||||
{isSelected && <Text dimColor> · enter to collapse</Text>}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user