Skip to content

Commit c311115

Browse files
committed
v0.7.25
- Fixed some shortcuts not working - Reorganized menu display to improve aesthetics and responsiveness
1 parent 9b90107 commit c311115

11 files changed

Lines changed: 256 additions & 32 deletions

File tree

.github/workflows/publish.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,8 @@ jobs:
5656
5757
### What's New
5858
59-
- Modify some incorrect prompt information
60-
- Add secondary confirmation and quick delete to history record deletion
61-
- Fix the bug of abnormal loading of history records when switching between welcome page and conversation page
62-
- Add anti-status loss mechanism to sub-agent and team mode
59+
- Fixed some shortcuts not working
60+
- Reorganized menu display to improve aesthetics and responsiveness
6361
6462
### Installation
6563
```bash

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "snow-ai",
3-
"version": "0.7.24",
3+
"version": "0.7.25",
44
"description": "Agentic coding in your terminal",
55
"license": "MIT",
66
"bin": {
@@ -12,7 +12,11 @@
1212
"ai",
1313
"assistant",
1414
"bot",
15-
"terminal"
15+
"terminal",
16+
"ai coding",
17+
"agentic",
18+
"snow",
19+
"snow cli"
1620
],
1721
"author": "Mufasa",
1822
"repository": {

source/cli.tsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -644,21 +644,20 @@ const Startup = ({
644644
// Store for cleanup
645645
(global as any).__deps = deps;
646646

647-
// Check for updates with timeout
648-
const updateCheckPromise = VERSION
649-
? checkForUpdates(VERSION)
650-
: Promise.resolve();
651-
652-
// Race between update check and 3-second timeout
653-
await Promise.race([
654-
updateCheckPromise,
655-
new Promise(resolve => setTimeout(resolve, 3000)),
656-
]);
657-
647+
// Render the app immediately once dependencies are ready.
648+
// The update check runs in the background to avoid blocking startup
649+
// when the network is slow/unreachable. WelcomeScreen subscribes to
650+
// onUpdateNotice and will render the notification UI once a result
651+
// is available.
658652
if (mounted) {
659653
setAppComponent(() => deps.App);
660654
setAppReady(true);
661655
}
656+
657+
// Fire-and-forget update check — never block app entry on network IO.
658+
if (VERSION) {
659+
void checkForUpdates(VERSION);
660+
}
662661
};
663662

664663
init();
@@ -856,3 +855,8 @@ const mainInk = render(
856855
patchConsole: true,
857856
},
858857
);
858+
859+
// Expose the Ink render handle so non-component code (e.g. the in-app
860+
// "Update Now" action in WelcomeScreen) can unmount Ink before handing the
861+
// terminal over to a child process such as `npm i -g snow-ai`.
862+
(global as any).__mainInk = mainInk;

source/i18n/lang/en.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ export const en: TranslationKeys = {
3838
updateNoticeLatest: 'Latest',
3939
updateNoticeRun: 'Run',
4040
updateNoticeGithub: 'GitHub',
41+
updateNow: 'Update Now',
42+
updateNowInfo:
43+
'Exit the CLI and run "npm i -g snow-ai" to upgrade to the latest version',
4144
exit: 'Exit',
4245
exitInfo: 'Exit the application',
4346
},

source/i18n/lang/zh-TW.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ export const zhTW: TranslationKeys = {
3535
updateNoticeLatest: '最新版本',
3636
updateNoticeRun: '更新指令',
3737
updateNoticeGithub: '專案網址',
38+
updateNow: '立即更新',
39+
updateNowInfo: '退出 CLI 並執行 "npm i -g snow-ai" 升級到最新版本',
3840
exit: '退出',
3941
exitInfo: '退出應用程式',
4042
},

source/i18n/lang/zh.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ export const zh: TranslationKeys = {
3535
updateNoticeLatest: '最新版本',
3636
updateNoticeRun: '更新命令',
3737
updateNoticeGithub: '项目地址',
38+
updateNow: '立即更新',
39+
updateNowInfo: '退出 CLI 并执行 "npm i -g snow-ai" 升级到最新版本',
3840
exit: '退出',
3941
exitInfo: '退出应用程序',
4042
},

source/i18n/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ export type TranslationKeys = {
3535
updateNoticeLatest: string;
3636
updateNoticeRun: string;
3737
updateNoticeGithub: string;
38+
updateNow: string;
39+
updateNowInfo: string;
3840
exit: string;
3941
exitInfo: string;
4042
};

source/ui/components/special/ChatHeader.tsx

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,43 +83,70 @@ export default function ChatHeader({
8383
);
8484
}
8585

86+
// 将 LOGO 字符串按可见字符数遮罩:未显示的可见字符替换为空格,换行保留,
87+
// 用于在保持布局稳定(行数/列宽不变)的前提下做"逐字显现"动画。
88+
// 当 revealChars 未传入或 >= 可见字符总数时,直接返回原始字符串。
89+
function maskRevealedChars(full: string, revealChars?: number): string {
90+
if (revealChars === undefined) return full;
91+
let visibleTotal = 0;
92+
for (const ch of full) {
93+
if (ch !== '\n') visibleTotal++;
94+
}
95+
if (revealChars >= visibleTotal) return full;
96+
let result = '';
97+
let revealed = 0;
98+
for (const ch of full) {
99+
if (ch === '\n') {
100+
result += ch;
101+
} else if (revealed < revealChars) {
102+
result += ch;
103+
revealed++;
104+
} else {
105+
result += ' ';
106+
}
107+
}
108+
return result;
109+
}
110+
86111
// Responsive ASCII art logo component for simple mode
87112
export function ChatHeaderLogo({
88113
terminalWidth,
89114
logoGradient,
90115
hideCompact = false,
116+
revealChars,
91117
}: {
92118
terminalWidth: number;
93119
logoGradient: [string, string, string];
94120
// 当为 true 时,宽度过窄(< 20)不再回退到最小 LOGO,而是直接不渲染。
95121
// 用于 WelcomeScreen 这种"位置紧张时宁可隐藏也不要降级展示"的场景。
96122
hideCompact?: boolean;
123+
// 控制 LOGO 已显示的可见字符数(不计换行)。未传入则始终完整显示。
124+
// 用于 WelcomeScreen 入场时的一次性逐字符出现动画。
125+
revealChars?: number;
97126
}) {
98127
if (terminalWidth >= 30) {
99128
// Full version: SNOW CLI with thin style (width >= 30)
129+
const fullLogo = `╔═╗╔╗╔╔═╗╦ ╦ ╔═╗╦ ╦
130+
╚═╗║║║║ ║║║║ ║ ║ ║
131+
╚═╝╝╚╝╚═╝╚╩╝ ╚═╝╩═╝╩`;
100132
return (
101133
<Box flexDirection="column" marginBottom={0}>
102134
<Gradient colors={logoGradient}>
103-
<Text>
104-
{`╔═╗╔╗╔╔═╗╦ ╦ ╔═╗╦ ╦
105-
╚═╗║║║║ ║║║║ ║ ║ ║
106-
╚═╝╝╚╝╚═╝╚╩╝ ╚═╝╩═╝╩`}
107-
</Text>
135+
<Text>{maskRevealedChars(fullLogo, revealChars)}</Text>
108136
</Gradient>
109137
</Box>
110138
);
111139
}
112140

113141
if (terminalWidth >= 20) {
114142
// Medium version: SNOW only (width 20-29)
143+
const mediumLogo = `╔═╗╔╗╔╔═╗╦ ╦
144+
╚═╗║║║║ ║║║║
145+
╚═╝╝╚╝╚═╝╚╩╝`;
115146
return (
116147
<Box flexDirection="column" marginBottom={0}>
117148
<Gradient colors={logoGradient}>
118-
<Text>
119-
{`╔═╗╔╗╔╔═╗╦ ╦
120-
╚═╗║║║║ ║║║║
121-
╚═╝╝╚╝╚═╝╚╩╝`}
122-
</Text>
149+
<Text>{maskRevealedChars(mediumLogo, revealChars)}</Text>
123150
</Gradient>
124151
</Box>
125152
);

source/ui/pages/WelcomeScreen.tsx

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {getUpdateNotice, onUpdateNotice} from '../../utils/ui/updateNotice.js';
1717
import {useTheme} from '../contexts/ThemeContext.js';
1818
import UpdateNotice from '../components/common/UpdateNotice.js';
1919
import {useTerminalTitle} from '../../hooks/ui/useTerminalTitle.js';
20+
import {runUpdateAndExit} from '../../utils/core/runUpdate.js';
2021

2122
// Lazy load all configuration screens for better startup performance
2223
const ConfigScreen = React.lazy(() => import('./ConfigScreen.js'));
@@ -46,6 +47,16 @@ const ThemeSettingsScreen = React.lazy(
4647
const HooksConfigScreen = React.lazy(() => import('./HooksConfigScreen.js'));
4748
const MCPConfigScreen = React.lazy(() => import('./MCPConfigScreen.js'));
4849

50+
// 模块级标志:保证 SNOW CLI LOGO 的逐字符出现动画在整个进程生命周期内只播放一次。
51+
// 任何后续的重渲染(菜单切换返回、终端 resize 触发的 remount 等)都直接显示完整 LOGO,
52+
// 不会再次触发动画。
53+
let hasPlayedLogoRevealAnimation = false;
54+
// LOGO 完整版可见字符总数(3 行 × 21 字符 = 63),用作 reveal 的上限。
55+
// 中等版(36)小于该值,所以同一个 totalChars 也能让中等版提前完成动画。
56+
const LOGO_REVEAL_MAX_CHARS = 63;
57+
// 每个字符出现的间隔时间(毫秒),决定动画的整体速度。
58+
const LOGO_REVEAL_INTERVAL_MS = 10;
59+
4960
type Props = {
5061
version?: string;
5162
onMenuSelect?: (value: string) => void;
@@ -85,6 +96,32 @@ export default function WelcomeScreen({
8596
const {columns: terminalWidth} = useTerminalSize();
8697
const {stdout} = useStdout();
8798
const isInitialMount = useRef(true);
99+
100+
// LOGO 逐字符出现动画:
101+
// - revealChars === undefined 表示动画已结束(或本次进程之前已播放过),完整显示。
102+
// - 数字值表示当前可见的字符数,会从 0 递增到 LOGO_REVEAL_MAX_CHARS。
103+
// 使用模块级 hasPlayedLogoRevealAnimation 保证只在首次进入时播放一次。
104+
const [logoRevealChars, setLogoRevealChars] = useState<number | undefined>(
105+
() => (hasPlayedLogoRevealAnimation ? undefined : 0),
106+
);
107+
useEffect(() => {
108+
if (hasPlayedLogoRevealAnimation) return;
109+
const interval = setInterval(() => {
110+
setLogoRevealChars(prev => {
111+
if (prev === undefined) return undefined;
112+
const next = prev + 1;
113+
if (next >= LOGO_REVEAL_MAX_CHARS) {
114+
clearInterval(interval);
115+
hasPlayedLogoRevealAnimation = true;
116+
// 切换为 undefined 让 ChatHeaderLogo 直接渲染完整字符串,
117+
// 后续重渲染不再走遮罩逻辑。
118+
return undefined;
119+
}
120+
return next;
121+
});
122+
}, LOGO_REVEAL_INTERVAL_MS);
123+
return () => clearInterval(interval);
124+
}, []);
88125
// 当终端宽度变化触发清屏时,先渲染为 null 一帧,把 ink/log-update 内部
89126
// "上一帧"缓存重置为空字符串;下一帧再切回 false 恢复完整内容,
90127
// 使新内容必然作为差异被完整写出,避免清屏后画面丢失。
@@ -113,6 +150,8 @@ export default function WelcomeScreen({
113150
return unsubscribe;
114151
}, []);
115152

153+
const hasUpdate = !!updateNotice;
154+
116155
const menuOptions = useMemo(
117156
() => [
118157
{
@@ -182,14 +221,27 @@ export default function WelcomeScreen({
182221
value: 'theme',
183222
infoText: t.welcome.themeSettingsInfo,
184223
},
224+
...(hasUpdate
225+
? [
226+
{
227+
label: `${t.welcome.updateNow}${
228+
updateNotice ? ` (v${updateNotice.latestVersion})` : ''
229+
}`,
230+
value: 'update-now',
231+
color: '#FFD700',
232+
infoText: t.welcome.updateNowInfo,
233+
clearTerminal: true,
234+
},
235+
]
236+
: []),
185237
{
186238
label: t.welcome.exit,
187239
value: 'exit',
188240
color: 'rgb(232, 131, 136)',
189241
infoText: t.welcome.exitInfo,
190242
},
191243
],
192-
[t],
244+
[t, hasUpdate, updateNotice],
193245
);
194246

195247
const [remountKey, setRemountKey] = useState(0);
@@ -250,6 +302,11 @@ export default function WelcomeScreen({
250302
setInlineView('language-settings');
251303
} else if (value === 'theme') {
252304
setInlineView('theme-settings');
305+
} else if (value === 'update-now') {
306+
// Hand the terminal over to npm: unmount Ink and exec the update.
307+
// runUpdateAndExit() does not return — the process exits when
308+
// the npm child finishes.
309+
runUpdateAndExit();
253310
} else {
254311
// Pass through to parent for other actions (chat, exit, etc.)
255312
onMenuSelect?.(value);
@@ -335,6 +392,11 @@ export default function WelcomeScreen({
335392
// 否则 ChatHeaderLogo 在 hideCompact 模式下会返回 null,留下一个空的右半区——
336393
// 此时直接把整个圆角框让给 Menu 占满,不再做左右拆分。
337394
const showLogoPane = logoColumnWidth >= 20;
395+
// 当右侧 LOGO 走"完整最大版"分支(terminalWidth >= 30,对应这里 logoColumnWidth >= 30)
396+
// 且存在更新提示时:把更新提示从顶部移到右侧 LOGO 下方,LOGO 区改为顶端对齐让 LOGO 上移,
397+
// 这样在宽终端下能更紧凑地利用右半区的垂直空间。
398+
const isFullLogoPane = showLogoPane && logoColumnWidth >= 30;
399+
const showUpdateNoticeInLogoPane = isFullLogoPane && !!updateNotice;
338400

339401
// 调整终端宽度后清屏的中间帧:渲染为 null,强制 log-update 把上一帧缓存
340402
// 重置为空字符串,下一帧的真实内容才能作为完整新内容被写出。
@@ -344,7 +406,7 @@ export default function WelcomeScreen({
344406

345407
return (
346408
<Box flexDirection="column" width={terminalWidth} key={remountKey}>
347-
{inlineView === 'menu' && updateNotice && (
409+
{inlineView === 'menu' && updateNotice && !showUpdateNoticeInLogoPane && (
348410
<UpdateNotice
349411
currentVersion={updateNotice.currentVersion}
350412
latestVersion={updateNotice.latestVersion}
@@ -389,21 +451,34 @@ export default function WelcomeScreen({
389451
</Box>
390452
<Box
391453
flexDirection="column"
392-
justifyContent="center"
454+
justifyContent={
455+
showUpdateNoticeInLogoPane ? 'flex-start' : 'center'
456+
}
393457
alignItems="center"
394458
paddingX={2}
459+
paddingY={showUpdateNoticeInLogoPane ? 1 : 0}
395460
flexGrow={1}
396461
>
397462
<ChatHeaderLogo
398463
terminalWidth={logoColumnWidth}
399464
logoGradient={theme.colors.logoGradient}
400465
hideCompact
466+
revealChars={logoRevealChars}
401467
/>
402468
<Box marginTop={1}>
403469
<Text color="gray" dimColor>
404470
v{version}{t.welcome.subtitle}
405471
</Text>
406472
</Box>
473+
{showUpdateNoticeInLogoPane && updateNotice && (
474+
<Box marginTop={1}>
475+
<UpdateNotice
476+
currentVersion={updateNotice.currentVersion}
477+
latestVersion={updateNotice.latestVersion}
478+
terminalWidth={logoColumnWidth}
479+
/>
480+
</Box>
481+
)}
407482
</Box>
408483
</>
409484
) : (

0 commit comments

Comments
 (0)