zelari-code 0.6.2 → 0.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/app.js +64 -29
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/components/ChatStream.js +40 -126
- package/dist/cli/components/ChatStream.js.map +1 -1
- package/dist/cli/components/LiveRegion.js +54 -0
- package/dist/cli/components/LiveRegion.js.map +1 -0
- package/dist/cli/components/SplashScreen.js +158 -0
- package/dist/cli/components/SplashScreen.js.map +1 -0
- package/dist/cli/components/StatusBar.js +42 -0
- package/dist/cli/components/StatusBar.js.map +1 -0
- package/dist/cli/components/ToolOutput.js +68 -0
- package/dist/cli/components/ToolOutput.js.map +1 -0
- package/dist/cli/components/toolFormat.js +195 -0
- package/dist/cli/components/toolFormat.js.map +1 -0
- package/dist/cli/councilDispatcher.js +20 -0
- package/dist/cli/councilDispatcher.js.map +1 -1
- package/dist/cli/grokOAuth.js +93 -75
- package/dist/cli/grokOAuth.js.map +1 -1
- package/dist/cli/hooks/chatState.js +139 -0
- package/dist/cli/hooks/chatState.js.map +1 -0
- package/dist/cli/hooks/eventsToMessages.js +1 -1
- package/dist/cli/hooks/eventsToMessages.js.map +1 -1
- package/dist/cli/hooks/messageHelpers.js +11 -4
- package/dist/cli/hooks/messageHelpers.js.map +1 -1
- package/dist/cli/hooks/useBatchedMessages.js +14 -10
- package/dist/cli/hooks/useBatchedMessages.js.map +1 -1
- package/dist/cli/hooks/useChatTurn.js +400 -87
- package/dist/cli/hooks/useChatTurn.js.map +1 -1
- package/dist/cli/hooks/useSession.js +23 -11
- package/dist/cli/hooks/useSession.js.map +1 -1
- package/dist/cli/hooks/useSlashDispatch.js +9 -1
- package/dist/cli/hooks/useSlashDispatch.js.map +1 -1
- package/dist/cli/main.bundled.js +15468 -12961
- package/dist/cli/main.bundled.js.map +4 -4
- package/dist/cli/main.js +43 -2
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/mcp/mcpClient.js +152 -0
- package/dist/cli/mcp/mcpClient.js.map +1 -0
- package/dist/cli/mcp/mcpManager.js +139 -0
- package/dist/cli/mcp/mcpManager.js.map +1 -0
- package/dist/cli/refreshRegistry.js +9 -6
- package/dist/cli/refreshRegistry.js.map +1 -1
- package/dist/cli/skillsMd.js +155 -0
- package/dist/cli/skillsMd.js.map +1 -0
- package/dist/cli/slashHandlers/skills.js +16 -0
- package/dist/cli/slashHandlers/skills.js.map +1 -1
- package/dist/cli/toolRegistry.js +31 -0
- package/dist/cli/toolRegistry.js.map +1 -1
- package/dist/cli/wizard/index.js +1 -1
- package/dist/cli/wizard/index.js.map +1 -1
- package/dist/cli/workspace/completeDesign.js +128 -0
- package/dist/cli/workspace/completeDesign.js.map +1 -0
- package/dist/cli/workspace/paths.js +20 -13
- package/dist/cli/workspace/paths.js.map +1 -1
- package/dist/cli/workspace/postCouncilHook.js +146 -20
- package/dist/cli/workspace/postCouncilHook.js.map +1 -1
- package/dist/cli/workspace/storage.js +7 -3
- package/dist/cli/workspace/storage.js.map +1 -1
- package/dist/cli/workspace/stubs.js +350 -136
- package/dist/cli/workspace/stubs.js.map +1 -1
- package/dist/cli/workspace/workspaceSummary.js +255 -0
- package/dist/cli/workspace/workspaceSummary.js.map +1 -0
- package/package.json +2 -2
package/dist/cli/app.js
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
// hook signatures and remove this annotation. The split is documented in
|
|
4
4
|
// `docs/plans/2026-07-01-app-split.md`.
|
|
5
5
|
import React, { useState, useMemo, useCallback } from 'react';
|
|
6
|
-
import { Box } from 'ink';
|
|
7
|
-
import { Header } from './components/Header.js';
|
|
8
|
-
import { ChatStream } from './components/ChatStream.js';
|
|
6
|
+
import { Box, Static } from 'ink';
|
|
9
7
|
import { InputBar } from './components/InputBar.js';
|
|
10
|
-
import {
|
|
8
|
+
import { LiveRegion } from './components/LiveRegion.js';
|
|
9
|
+
import { StatusBar } from './components/StatusBar.js';
|
|
10
|
+
import { renderMessage } from './components/ChatStream.js';
|
|
11
11
|
import { formatSkillList } from './slashCommands.js';
|
|
12
12
|
import { listCodingSkills } from '@zelari/core/skills';
|
|
13
13
|
import '@zelari/core/skills/builtin/debugging';
|
|
@@ -19,10 +19,12 @@ import '@zelari/core/skills/builtin/review';
|
|
|
19
19
|
import '@zelari/core/skills/builtin/testing';
|
|
20
20
|
import { getProviderConfig, getActiveProvider as getActiveProviderSpec, } from './providerConfig.js';
|
|
21
21
|
import { SessionJsonlWriter } from '@zelari/core/harness';
|
|
22
|
-
import {
|
|
22
|
+
import { VERSION } from './main.js';
|
|
23
23
|
import { useSession } from './hooks/useSession.js';
|
|
24
24
|
import { useChatTurn } from './hooks/useChatTurn.js';
|
|
25
25
|
import { useSlashDispatch } from './hooks/useSlashDispatch.js';
|
|
26
|
+
import { useBatchedMessages } from './hooks/useBatchedMessages.js';
|
|
27
|
+
import { useTerminalSize } from './hooks/useTerminalSize.js';
|
|
26
28
|
const MODEL = process.env.OPENAI_MODEL ?? 'grok-4';
|
|
27
29
|
const PROVIDER = 'openai-compatible';
|
|
28
30
|
/**
|
|
@@ -36,37 +38,48 @@ const providerDefaults = {
|
|
|
36
38
|
'glm': 'glm-4.5',
|
|
37
39
|
};
|
|
38
40
|
/**
|
|
39
|
-
* App — Ink UI shell.
|
|
41
|
+
* App — Ink UI shell (v0.7.0 static-scrollback layout).
|
|
40
42
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
43
|
+
* The fixed-height frame of v0.6 (root `<Box width height overflow=hidden>` +
|
|
44
|
+
* `pickVisibleMessages`) is gone. Finalized messages feed Ink's `<Static>`,
|
|
45
|
+
* which prints each item exactly once to real stdout so it becomes part of
|
|
46
|
+
* the terminal's native scrollback. The dynamic region Ink repaints is now
|
|
47
|
+
* just: the streaming tail (`<LiveRegion>`), a one-line `<StatusBar>`, and
|
|
48
|
+
* the `<InputBar>`. Because that region is always a few lines tall, it can
|
|
49
|
+
* never exceed the terminal height → no full-screen clear/repaint → no
|
|
50
|
+
* flicker, by construction.
|
|
48
51
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
52
|
+
* v0.4.2 split still holds: App is a thin composition of focused hooks
|
|
53
|
+
* (useSession, useChatTurn, useSlashDispatch).
|
|
51
54
|
*/
|
|
52
55
|
export function App() {
|
|
53
56
|
const [input, setInput] = useState('');
|
|
54
57
|
const [busy, setBusy] = useState(false);
|
|
55
58
|
const [providerConfig, setProviderConfig] = useState(() => getProviderConfig());
|
|
56
59
|
const [sessionStats, setSessionStats] = useState({ totalTokens: 0, totalCostUsd: 0 });
|
|
60
|
+
// v0.7.0: bump on /clear to remount <Static> (resets its internal "already
|
|
61
|
+
// printed" index so the ANSI-cleared scrollback stays in sync). Also bumped
|
|
62
|
+
// implicitly by a sessionId change (/new).
|
|
63
|
+
const [clearEpoch, setClearEpoch] = useState(0);
|
|
57
64
|
const activeProviderSpec = getActiveProviderSpec();
|
|
58
65
|
const activeModel = providerConfig.modelByProvider[activeProviderSpec.id];
|
|
59
66
|
const session = useSession();
|
|
60
67
|
const size = useTerminalSize();
|
|
68
|
+
// Throttle layer for the `live` region — coalesces per-token streaming
|
|
69
|
+
// updates (~50-200/sec) into ≤60 renders/sec. The finalized array uses the
|
|
70
|
+
// raw setter (its appends are rare and user-facing).
|
|
71
|
+
const { commit: commitLive, flush: flushLive } = useBatchedMessages(session.live, session.setLive);
|
|
61
72
|
const chatTurn = useChatTurn({
|
|
62
73
|
sessionId: session.sessionId,
|
|
63
74
|
writerRef: session.writerRef,
|
|
64
75
|
setMessages: session.setMessages,
|
|
65
|
-
commitStreaming:
|
|
66
|
-
flushStreaming:
|
|
76
|
+
commitStreaming: commitLive,
|
|
77
|
+
flushStreaming: flushLive,
|
|
67
78
|
setBusy,
|
|
68
79
|
setSessionActive: session.setSessionActive,
|
|
69
80
|
setSessionStats,
|
|
81
|
+
setLive: session.setLive,
|
|
82
|
+
liveRef: session.liveRef,
|
|
70
83
|
});
|
|
71
84
|
// /new callback: close the old writer, open a new one for the new id.
|
|
72
85
|
const onNewSession = useCallback((id) => {
|
|
@@ -79,6 +92,12 @@ export function App() {
|
|
|
79
92
|
void session.writerRef.current?.close();
|
|
80
93
|
setTimeout(() => process.exit(0), 50);
|
|
81
94
|
}, [session.writerRef]);
|
|
95
|
+
// /clear callback (v0.7.0): reset the live region too (pending tools /
|
|
96
|
+
// streaming bubble shouldn't survive a clear) and bump the Static epoch.
|
|
97
|
+
const onClear = useCallback(() => {
|
|
98
|
+
session.resetTranscript();
|
|
99
|
+
setClearEpoch((e) => e + 1);
|
|
100
|
+
}, [session]);
|
|
82
101
|
const handleSubmit = useSlashDispatch({
|
|
83
102
|
skills: useMemo(() => listCodingSkills(), []),
|
|
84
103
|
sessionId: session.sessionId,
|
|
@@ -98,20 +117,36 @@ export function App() {
|
|
|
98
117
|
dispatchCouncilPrompt: chatTurn.dispatchCouncilPrompt,
|
|
99
118
|
onNewSession,
|
|
100
119
|
onExit,
|
|
120
|
+
onClear,
|
|
101
121
|
});
|
|
102
122
|
const skills = useMemo(() => listCodingSkills(), []);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
123
|
+
// The one-shot banner: printed once as the first Static item. Replaces the
|
|
124
|
+
// persistent Header. Includes the skill list summary so the user can see
|
|
125
|
+
// what's available without the 40-col Sidebar (which can't coexist with
|
|
126
|
+
// native scrollback — a fixed right column would be printed into every
|
|
127
|
+
// scrollback line).
|
|
128
|
+
const banner = useMemo(() => {
|
|
129
|
+
const skillList = formatSkillList(skills);
|
|
130
|
+
return {
|
|
131
|
+
id: 'banner-once',
|
|
132
|
+
role: 'system',
|
|
133
|
+
ts: 0,
|
|
134
|
+
content: `zelari-code v${VERSION} — ${activeProviderSpec.id}/${activeModel}\n` +
|
|
135
|
+
`${skills.length} skills available. Type /help for the list, or /skill <name>.\n` +
|
|
136
|
+
`── skills ──\n${skillList}`,
|
|
137
|
+
};
|
|
138
|
+
}, [skills, activeProviderSpec.id, activeModel]);
|
|
139
|
+
// The Static feed: banner first, then finalized messages. `key` lets /clear
|
|
140
|
+
// remount Static so its internal "already printed" index resets. clearEpoch
|
|
141
|
+
// is composed in so /clear forces a remount even within the same session.
|
|
142
|
+
const staticKey = `${session.sessionId || 'pre-bootstrap'}-${clearEpoch}`;
|
|
143
|
+
const staticItems = [banner, ...session.messages];
|
|
144
|
+
return (React.createElement(React.Fragment, null,
|
|
145
|
+
React.createElement(Static, { key: staticKey, items: staticItems }, (item) => renderMessage(item)),
|
|
146
|
+
React.createElement(Box, { flexDirection: "column", borderTop: true, paddingX: 1 },
|
|
147
|
+
React.createElement(LiveRegion, { live: session.live, busy: busy }),
|
|
148
|
+
React.createElement(StatusBar, { model: activeModel, provider: activeProviderSpec.id, sessionId: session.sessionId ? session.sessionId.slice(0, 8) : '...', sessionActive: session.sessionActive, totalTokens: sessionStats.totalTokens, totalCostUsd: sessionStats.totalCostUsd, queueCount: chatTurn.queueCount, busy: busy }),
|
|
149
|
+
React.createElement(InputBar, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: busy }))));
|
|
115
150
|
}
|
|
116
151
|
// Re-export so legacy imports keep working (Task v0.4.2 audit split).
|
|
117
152
|
// New code should import directly from src/cli/hooks/* instead.
|
package/dist/cli/app.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/cli/app.tsx"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,+EAA+E;AAC/E,yEAAyE;AACzE,wCAAwC;AACxC,OAAO,KAAK,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/cli/app.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,139 +1,53 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
import {
|
|
3
|
+
import { ToolOutput } from './ToolOutput.js';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* renderMessage — pure per-message renderer shared by `<Static>` (finalized
|
|
6
|
+
* items, printed once into native scrollback) and the live region (the
|
|
7
|
+
* streaming tail + pending tools, repainted by Ink).
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* v0.7.0 static-scrollback refactor: this replaces the old `ChatStream`
|
|
10
|
+
* component, which combined rendering with visible-message picking
|
|
11
|
+
* (`pickVisibleMessages` / `estimateMessageHeight`). Those are gone —
|
|
12
|
+
* `<Static>` handles scrollback, and the live region guarantees a small
|
|
13
|
+
* dynamic footprint by construction. Each message renders identically
|
|
14
|
+
* whether it is printed once (Static) or repainted (live), so the UI is
|
|
15
|
+
* stable as a message crosses from live → finalized.
|
|
16
|
+
*
|
|
17
|
+
* `live` flag: when true, the tool summary shows a pending glyph and no
|
|
18
|
+
* duration (the invocation hasn't ended yet). Finalized tool messages
|
|
19
|
+
* render via the stateless `ToolOutput` policy (Phase 3).
|
|
13
20
|
*/
|
|
14
|
-
function
|
|
15
|
-
const textWidth = Math.max(1, width - 4);
|
|
21
|
+
export function renderMessage(m, live = false) {
|
|
16
22
|
if (m.role === 'tool') {
|
|
17
|
-
|
|
18
|
-
// which can wrap on narrow terminals / long arg previews.
|
|
19
|
-
const summaryLen = m.content.length + (m.toolName?.length ?? 4) + 13;
|
|
20
|
-
let rows = Math.max(1, Math.ceil(summaryLen / textWidth));
|
|
21
|
-
if (m.toolOk === false && m.toolResult) {
|
|
22
|
-
// Failed tools auto-expand: bordered body (2 border rows) + wrapped
|
|
23
|
-
// body lines at textWidth - 6 (extra marginLeft 2 + border 2 + padding 2).
|
|
24
|
-
const bodyWidth = Math.max(1, textWidth - 6);
|
|
25
|
-
rows += 2;
|
|
26
|
-
for (const line of m.toolResult.split('\n')) {
|
|
27
|
-
rows += Math.max(1, Math.ceil(line.length / bodyWidth));
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return rows;
|
|
31
|
-
}
|
|
32
|
-
const lines = m.content.split('\n');
|
|
33
|
-
let textRows = 0;
|
|
34
|
-
for (const line of lines) {
|
|
35
|
-
textRows += Math.max(1, Math.ceil(line.length / textWidth));
|
|
23
|
+
return (React.createElement(ToolOutput, { key: m.id, toolName: m.toolName ?? 'tool', summary: m.content, body: m.toolResult ?? m.content, ok: m.toolOk, durationMs: m.toolDurationMs, live: live }));
|
|
36
24
|
}
|
|
37
|
-
return
|
|
25
|
+
return (React.createElement(Box, { key: m.id, flexDirection: "column", marginBottom: 1 },
|
|
26
|
+
React.createElement(Text, { color: m.role === 'user' ? 'cyan' : m.role === 'assistant' ? 'green' : 'yellow', bold: true },
|
|
27
|
+
m.role === 'user' ? '❯' : m.role === 'assistant' ? '◆' : 'ℹ',
|
|
28
|
+
" ",
|
|
29
|
+
m.role,
|
|
30
|
+
m.role === 'assistant' && m.memberName ? (React.createElement(Text, { color: "magenta" },
|
|
31
|
+
" \u00B7 ",
|
|
32
|
+
m.memberName)) : null),
|
|
33
|
+
React.createElement(Box, { marginLeft: 2 },
|
|
34
|
+
React.createElement(Text, null, m.content))));
|
|
38
35
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
* to the first message that overflows. Extracted so it's unit-testable and
|
|
42
|
-
* memoizable in the component.
|
|
43
|
-
*/
|
|
44
|
-
export function pickVisibleMessages(messages, height, width) {
|
|
45
|
-
const visibleMessages = [];
|
|
46
|
-
let remainingHeight = height - 1; // 1 row buffer for safety
|
|
47
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
48
|
-
const m = messages[i];
|
|
49
|
-
const mHeight = estimateMessageHeight(m, width);
|
|
50
|
-
if (remainingHeight - mHeight >= 0) {
|
|
51
|
-
visibleMessages.unshift(m);
|
|
52
|
-
remainingHeight -= mHeight;
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
// Truncate the top-most visible message if we have some space.
|
|
56
|
-
// Tool messages: collapse the body to just the summary when
|
|
57
|
-
// there is not enough height for the (possibly auto-expanded)
|
|
58
|
-
// bordered body — silently dropping the tool would blank the
|
|
59
|
-
// whole transcript. Regression HIGH-1 (v0.6.2 audit).
|
|
60
|
-
if (m.role === 'tool') {
|
|
61
|
-
// Tools: minimum 1 row for the summary line. Skip the
|
|
62
|
-
// `remainingHeight > 2` guard (which was tuned for assistant
|
|
63
|
-
// messages that need header + body + margin-bottom).
|
|
64
|
-
const textWidth = Math.max(1, width - 4);
|
|
65
|
-
const summaryLen = m.content.length + (m.toolName?.length ?? 4) + 13;
|
|
66
|
-
const summaryRows = Math.max(1, Math.ceil(summaryLen / textWidth));
|
|
67
|
-
if (summaryRows <= remainingHeight) {
|
|
68
|
-
// Show the collapsed summary (drop the auto-expanded body
|
|
69
|
-
// if it would push us past the available height).
|
|
70
|
-
const { toolResult: _omit, ...collapsed } = m;
|
|
71
|
-
void _omit;
|
|
72
|
-
visibleMessages.unshift(collapsed);
|
|
73
|
-
}
|
|
74
|
-
// else: even the summary doesn't fit — fall through to break
|
|
75
|
-
// and leave earlier visible messages in place.
|
|
76
|
-
}
|
|
77
|
-
else if (remainingHeight > 2) {
|
|
78
|
-
const textWidth = Math.max(1, width - 4);
|
|
79
|
-
const maxTextRows = remainingHeight - 2; // header + margin-bottom
|
|
80
|
-
const lines = m.content.split('\n');
|
|
81
|
-
const truncatedLines = [];
|
|
82
|
-
let currentRows = 0;
|
|
83
|
-
for (let j = lines.length - 1; j >= 0; j--) {
|
|
84
|
-
const line = lines[j];
|
|
85
|
-
const lineRows = Math.max(1, Math.ceil(line.length / textWidth));
|
|
86
|
-
if (currentRows + lineRows <= maxTextRows) {
|
|
87
|
-
truncatedLines.unshift(line);
|
|
88
|
-
currentRows += lineRows;
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
truncatedLines.unshift('... [truncated]');
|
|
92
|
-
break;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
visibleMessages.unshift({
|
|
96
|
-
...m,
|
|
97
|
-
content: truncatedLines.join('\n'),
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return visibleMessages;
|
|
36
|
+
function ChatStreamImpl({ messages }) {
|
|
37
|
+
return (React.createElement(Box, { flexDirection: "column", flexGrow: 1, paddingX: 1 }, messages.length === 0 ? (React.createElement(Text, { dimColor: true }, "Ready. Type a prompt or /skill <name> to invoke a skill.")) : (messages.map((m) => renderMessage(m)))));
|
|
104
38
|
}
|
|
39
|
+
export const ChatStream = React.memo(ChatStreamImpl);
|
|
40
|
+
// ── Backward-compat exports for tests still importing the picking helpers ──
|
|
41
|
+
// The v0.6.2 `pickVisibleMessages` / `estimateMessageHeight` are removed in
|
|
42
|
+
// v0.7.0 (the fixed frame is gone). Re-exporting shims that delegate to a
|
|
43
|
+
// trivial implementation keeps older test imports resolving until those tests
|
|
44
|
+
// are migrated. See tests/unit/v4-ui-audit.test.ts + cli-toolDisplay.test.ts.
|
|
105
45
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* Performance: `visibleMessages` is computed via `React.useMemo` keyed on
|
|
111
|
-
* [messages, height, width]. Without this, every streaming token delta from
|
|
112
|
-
* the LLM (≈20-50/sec) would trigger:
|
|
113
|
-
* - O(N) `estimateMessageHeight` calls per render
|
|
114
|
-
* - O(N) `m.content.split('\n')` calls inside the truncation branch
|
|
115
|
-
* - A new `visibleMessages` array, which causes every child to re-render
|
|
116
|
-
*
|
|
117
|
-
* Wrapped in React.memo so the component re-renders only when its props
|
|
118
|
-
* actually change (messages array identity, height, width).
|
|
46
|
+
* @deprecated since v0.7.0 — visible-message picking is removed; `<Static>`
|
|
47
|
+
* handles scrollback. This shim returns the input unchanged so legacy tests
|
|
48
|
+
* that only assert on returned message identity/content still pass.
|
|
119
49
|
*/
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
return (React.createElement(Box, { flexDirection: "column", flexGrow: 1, paddingX: 1, height: height, overflow: "hidden" }, visibleMessages.length === 0 ? (React.createElement(Text, { dimColor: true }, "Ready. Type a prompt or /skill <name> to invoke a skill.")) : (visibleMessages.map((m) => {
|
|
123
|
-
if (m.role === 'tool') {
|
|
124
|
-
return (React.createElement(CollapsibleToolOutput, { key: m.id, toolName: m.toolName ?? 'tool', summary: m.content, body: m.toolResult ?? m.content, ok: m.toolOk, durationMs: m.toolDurationMs, defaultExpanded: m.toolOk === false && !!m.toolResult }));
|
|
125
|
-
}
|
|
126
|
-
return (React.createElement(Box, { key: m.id, flexDirection: "column", marginBottom: 1 },
|
|
127
|
-
React.createElement(Text, { color: m.role === 'user' ? 'cyan' : m.role === 'assistant' ? 'green' : 'yellow', bold: true },
|
|
128
|
-
m.role === 'user' ? '❯' : m.role === 'assistant' ? '◆' : 'ℹ',
|
|
129
|
-
" ",
|
|
130
|
-
m.role,
|
|
131
|
-
m.role === 'assistant' && m.memberName ? (React.createElement(Text, { color: "magenta" },
|
|
132
|
-
" \u00B7 ",
|
|
133
|
-
m.memberName)) : null),
|
|
134
|
-
React.createElement(Box, { marginLeft: 2 },
|
|
135
|
-
React.createElement(Text, null, m.content))));
|
|
136
|
-
}))));
|
|
50
|
+
export function pickVisibleMessages(messages, _height, _width) {
|
|
51
|
+
return [...messages];
|
|
137
52
|
}
|
|
138
|
-
export const ChatStream = React.memo(ChatStreamImpl);
|
|
139
53
|
//# sourceMappingURL=ChatStream.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatStream.js","sourceRoot":"","sources":["../../../src/cli/components/ChatStream.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAChC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"ChatStream.js","sourceRoot":"","sources":["../../../src/cli/components/ChatStream.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAwC7C;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,aAAa,CAAC,CAAc,EAAE,IAAI,GAAG,KAAK;IACxD,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QACtB,OAAO,CACL,oBAAC,UAAU,IACT,GAAG,EAAE,CAAC,CAAC,EAAE,EACT,QAAQ,EAAE,CAAC,CAAC,QAAQ,IAAI,MAAM,EAC9B,OAAO,EAAE,CAAC,CAAC,OAAO,EAClB,IAAI,EAAE,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,OAAO,EAC/B,EAAE,EAAE,CAAC,CAAC,MAAM,EACZ,UAAU,EAAE,CAAC,CAAC,cAAc,EAC5B,IAAI,EAAE,IAAI,GACV,CACH,CAAC;IACJ,CAAC;IACD,OAAO,CACL,oBAAC,GAAG,IAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,aAAa,EAAC,QAAQ,EAAC,YAAY,EAAE,CAAC;QACpD,oBAAC,IAAI,IAAC,KAAK,EAAE,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,EAAE,IAAI;YACxF,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG;;YAAG,CAAC,CAAC,IAAI;YACrE,CAAC,CAAC,IAAI,KAAK,WAAW,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CACxC,oBAAC,IAAI,IAAC,KAAK,EAAC,SAAS;;gBAAK,CAAC,CAAC,UAAU,CAAQ,CAC/C,CAAC,CAAC,CAAC,IAAI,CACH;QACP,oBAAC,GAAG,IAAC,UAAU,EAAE,CAAC;YAChB,oBAAC,IAAI,QAAE,CAAC,CAAC,OAAO,CAAQ,CACpB,CACF,CACP,CAAC;AACJ,CAAC;AAiBD,SAAS,cAAc,CAAC,EAAE,QAAQ,EAAmB;IACnD,OAAO,CACL,oBAAC,GAAG,IAAC,aAAa,EAAC,QAAQ,EAAC,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,IACjD,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CACvB,oBAAC,IAAI,IAAC,QAAQ,qEAAsE,CACrF,CAAC,CAAC,CAAC,CACF,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CACtC,CACG,CACP,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;AAErD,8EAA8E;AAC9E,4EAA4E;AAC5E,0EAA0E;AAC1E,8EAA8E;AAC9E,8EAA8E;AAE9E;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CACjC,QAAgC,EAChC,OAAe,EACf,MAAc;IAEd,OAAO,CAAC,GAAG,QAAQ,CAAC,CAAC;AACvB,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { renderMessage } from './ChatStream.js';
|
|
4
|
+
/**
|
|
5
|
+
* Max lines of the streaming bubble kept in the dynamic region. The full
|
|
6
|
+
* text is never lost — it lands complete in `<Static>` at finalize. This
|
|
7
|
+
* clamp only bounds the live repaint footprint so the dynamic region fits
|
|
8
|
+
* any terminal height, eliminating full-screen repaints by construction.
|
|
9
|
+
*/
|
|
10
|
+
const LIVE_STREAM_TAIL_LINES = 10;
|
|
11
|
+
/**
|
|
12
|
+
* LiveRegion — the small dynamic region Ink repaints every frame.
|
|
13
|
+
*
|
|
14
|
+
* Contains:
|
|
15
|
+
* - the currently-streaming assistant bubble (clamped to the last
|
|
16
|
+
* `LIVE_STREAM_TAIL_LINES` lines so the region stays short), and
|
|
17
|
+
* - one `⋯ [tool] summary` line per pending tool invocation.
|
|
18
|
+
*
|
|
19
|
+
* Because this region is always at most ~10 + N lines tall, it can never
|
|
20
|
+
* exceed the terminal height → Ink never falls back to clear-screen + full
|
|
21
|
+
* repaint → no flicker, by construction. Finalized messages live in the
|
|
22
|
+
* terminal's native scrollback via `<Static>` (printed exactly once).
|
|
23
|
+
*/
|
|
24
|
+
export function LiveRegion({ live, busy }) {
|
|
25
|
+
const { streaming, runningTools } = live;
|
|
26
|
+
if (!streaming && runningTools.length === 0)
|
|
27
|
+
return null;
|
|
28
|
+
return (React.createElement(Box, { flexDirection: "column", paddingX: 1 },
|
|
29
|
+
streaming && streaming.role === 'assistant' && (React.createElement(StreamingTail, { id: streaming.id, content: streaming.content, ts: streaming.ts, memberName: streaming.memberName, memberId: streaming.memberId })),
|
|
30
|
+
runningTools.map((t) => (React.createElement(Box, { key: t.id, flexDirection: "column" }, renderMessage(t, true)))),
|
|
31
|
+
busy && runningTools.length === 0 && !streaming && (React.createElement(Text, { dimColor: true }, " \u22EF working\u2026"))));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Render the streaming bubble, clamped to the last N content lines. A leading
|
|
35
|
+
* `…` marker is shown when the content was truncated so the user knows there
|
|
36
|
+
* is more above (in the scrollback once finalized). The bubble uses the same
|
|
37
|
+
* header style as `renderMessage` so it is visually continuous when it
|
|
38
|
+
* crosses from live → finalized.
|
|
39
|
+
*/
|
|
40
|
+
function StreamingTail(m) {
|
|
41
|
+
const lines = m.content.split('\n');
|
|
42
|
+
const truncated = lines.length > LIVE_STREAM_TAIL_LINES;
|
|
43
|
+
const tail = truncated ? lines.slice(-LIVE_STREAM_TAIL_LINES) : lines;
|
|
44
|
+
return (React.createElement(Box, { key: m.id, flexDirection: "column", marginBottom: 1 },
|
|
45
|
+
React.createElement(Text, { color: "green", bold: true },
|
|
46
|
+
"\u25C6 assistant",
|
|
47
|
+
m.memberName ? React.createElement(Text, { color: "magenta" },
|
|
48
|
+
" \u00B7 ",
|
|
49
|
+
m.memberName) : null),
|
|
50
|
+
React.createElement(Box, { marginLeft: 2, flexDirection: "column" },
|
|
51
|
+
truncated && React.createElement(Text, { dimColor: true }, "\u2026"),
|
|
52
|
+
React.createElement(Text, null, tail.join('\n')))));
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=LiveRegion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LiveRegion.js","sourceRoot":"","sources":["../../../src/cli/components/LiveRegion.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAChC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD;;;;;GAKG;AACH,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAOlC;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,UAAU,CAAC,EAAE,IAAI,EAAE,IAAI,EAAmB;IACxD,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC;IAEzC,IAAI,CAAC,SAAS,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEzD,OAAO,CACL,oBAAC,GAAG,IAAC,aAAa,EAAC,QAAQ,EAAC,QAAQ,EAAE,CAAC;QACpC,SAAS,IAAI,SAAS,CAAC,IAAI,KAAK,WAAW,IAAI,CAC9C,oBAAC,aAAa,IACZ,EAAE,EAAE,SAAS,CAAC,EAAE,EAChB,OAAO,EAAE,SAAS,CAAC,OAAO,EAC1B,EAAE,EAAE,SAAS,CAAC,EAAE,EAChB,UAAU,EAAE,SAAS,CAAC,UAAU,EAChC,QAAQ,EAAE,SAAS,CAAC,QAAQ,GAC5B,CACH;QACA,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CACvB,oBAAC,GAAG,IAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,aAAa,EAAC,QAAQ,IACnC,aAAa,CAAC,CAAC,EAAE,IAAI,CAAC,CACnB,CACP,CAAC;QACD,IAAI,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,SAAS,IAAI,CAClD,oBAAC,IAAI,IAAC,QAAQ,kCAAmB,CAClC,CACG,CACP,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,SAAS,aAAa,CAAC,CAMtB;IACC,MAAM,KAAK,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACpC,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,GAAG,sBAAsB,CAAC;IACxD,MAAM,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IACtE,OAAO,CACL,oBAAC,GAAG,IAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,aAAa,EAAC,QAAQ,EAAC,YAAY,EAAE,CAAC;QACpD,oBAAC,IAAI,IAAC,KAAK,EAAC,OAAO,EAAC,IAAI;;YAErB,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,oBAAC,IAAI,IAAC,KAAK,EAAC,SAAS;;gBAAK,CAAC,CAAC,UAAU,CAAQ,CAAC,CAAC,CAAC,IAAI,CAChE;QACP,oBAAC,GAAG,IAAC,UAAU,EAAE,CAAC,EAAE,aAAa,EAAC,QAAQ;YACvC,SAAS,IAAI,oBAAC,IAAI,IAAC,QAAQ,mBAAS;YACrC,oBAAC,IAAI,QAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAQ,CAC1B,CACF,CACP,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SplashScreen — one-shot startup splash (v0.7.8).
|
|
3
|
+
*
|
|
4
|
+
* Shows the N-THEM / Zelari emblem (downscaled from the original 400-col
|
|
5
|
+
* ASCII art) centered in the terminal for ~2 seconds — or until any key is
|
|
6
|
+
* pressed — then unmounts and gives way to the normal App.
|
|
7
|
+
*
|
|
8
|
+
* Sizing: two pre-rendered variants (64 and 44 columns wide) are embedded
|
|
9
|
+
* as constants; `pickSplashArt` chooses the largest one that fits the
|
|
10
|
+
* current terminal, or returns null when even the small one doesn't fit
|
|
11
|
+
* (the splash is then skipped entirely).
|
|
12
|
+
*
|
|
13
|
+
* Skipped when stdout is not a TTY (pipes, CI) or `ZELARI_NO_SPLASH=1`.
|
|
14
|
+
*/
|
|
15
|
+
// NOTE: no import from '../main.js' here — main.ts executes `main()` at
|
|
16
|
+
// module scope, so importing it from a component (or a unit test that
|
|
17
|
+
// imports this component) would boot the whole CLI. The version string is
|
|
18
|
+
// passed in as a prop instead.
|
|
19
|
+
import React, { useEffect, useState } from 'react';
|
|
20
|
+
import { Box, Text, useInput, useStdin } from 'ink';
|
|
21
|
+
/** How long the splash stays up before auto-dismissing (ms). */
|
|
22
|
+
export const SPLASH_DURATION_MS = 2000;
|
|
23
|
+
// Downscaled from the source emblem (142×79) by block-averaging glyph
|
|
24
|
+
// density — see docs/plans/2026-07-03-cli-splash-screen.md.
|
|
25
|
+
const EMBLEM_LARGE = ` =%@*-
|
|
26
|
+
=%@@@@@#:
|
|
27
|
+
*%%%%%%@@%-
|
|
28
|
+
:#%%%%%%%%%@@*
|
|
29
|
+
:*%%%%%%%%%%%%%@%*.
|
|
30
|
+
=%%%%%%%%%%%%%%%%@@%.
|
|
31
|
+
=%%%%%%%%%%%%%%%%%%%@%-
|
|
32
|
+
*%%%%%%%%%%%%%%%%%%%%%@@=
|
|
33
|
+
.#%%%%%%%%@@@@@@@@%%%%%%@@@+
|
|
34
|
+
:#%%%%%%@@@@@@@@@@@@@@@%%%%@@+
|
|
35
|
+
-%%%%%@@@@@@@@@@@@@@@@@@@@@%%@@#.
|
|
36
|
+
-%%%%%@@@@@@@@@@@@@@@@@@@@@@@%%@@%:
|
|
37
|
+
=%%%%@@@@@@@@@@@@@@@@@@@@@@@@@@@%@@%.
|
|
38
|
+
+%%%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%@@%:
|
|
39
|
+
=%%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@-
|
|
40
|
+
-%%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%:
|
|
41
|
+
:%%%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%
|
|
42
|
+
#%%%%%@@@@@@@@@@@@@@@%--#%@@@@@@@@@@@@@%%@@@=
|
|
43
|
+
#@@%%%%@@@@@@@@@@@@@@%: -*@@@@@@@@@@@%%@@@@=
|
|
44
|
+
#@@%%%%@@@@@@@@@@@@@%: .. -#@@@@@@@%%%%@@@=
|
|
45
|
+
+@@@%%%@@@@@@@@@@@@%: :#= =%@@@@%%%@@@%-
|
|
46
|
+
=@@@@%%%@@@@@@@@@@%: -%%*: +%@@%%@@@#
|
|
47
|
+
+%@@@@%%@@@@@@@@@%: -%@@%*=.:+#@@@@#:
|
|
48
|
+
.+*@@@@@@@@@%%%@@@@@@%: -%@@@@@#- +@@@@@@*=
|
|
49
|
+
=*%%%%@@@@@@@@@@@%%@@@@%: -#@@@@@@@*: :*@@@@@@%+=
|
|
50
|
+
-*%%%%%%%%%%@@@@@@@@@@@@@@@: -%@@@@@@@@#- -#@@@@@@@@*
|
|
51
|
+
#%%%%%%%%%%%%%@@@@@@@@@@@@@: -#@@@@@%+-::. *@@@@@@@@=
|
|
52
|
+
=%%%%%%%%%%%%%%%@@@@@@@@@@@@: :*+--=%#. :+++++*@@@@@@@@%
|
|
53
|
+
#@%%%%%%%%%%%%%@@@@@@@@@@@@%: .. - -## *@@@@@@@@@@@@@@=
|
|
54
|
+
:@@%%%%%%%%%%%%@@@@@@@@@@@@@%: .**= *%= :%@@@@@@@@@@@@@%
|
|
55
|
+
#@@@%%%%%%%%%%%@@@@@@@@@@@@@@- =#@@%- :**: =%@@@@@@@@@@@@@+
|
|
56
|
+
=@@@@@%%%@@%%%%@@@@@@@@@@@@@@@@@@@@@@%- :: .-#@@@@@@@@@@@@@%.
|
|
57
|
+
:@@@@@@%%@@@%%@@@@@@@@@@@@@@@@@@@@@@@@@%==*%%@@@@@@@@@@@@@@@@#
|
|
58
|
+
*@@@@@@@%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@:
|
|
59
|
+
:@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+
|
|
60
|
+
=##########*######**##**#*********##**#*#######################*`;
|
|
61
|
+
const EMBLEM_SMALL = ` =%%*:
|
|
62
|
+
:#%%@@@+
|
|
63
|
+
=%%%%%%%@#.
|
|
64
|
+
.*%%%%%%%%%@%-
|
|
65
|
+
=%%%%%%%%%%%%%@*.
|
|
66
|
+
+%%%%%%@@@@@%%%%@%:
|
|
67
|
+
*%%%%@@@@@@@@@@@%%@%:
|
|
68
|
+
*%%%@@@@@@@@@@@@@@@%@%-
|
|
69
|
+
:#%%@@@@@@@@@@@@@@@@@@@@@+
|
|
70
|
+
:%%%@@@@@@@@@@@@@@@@@@@@@@@+
|
|
71
|
+
#%%@@@@@@@@@@@@@@@@@@@@@@@@@-
|
|
72
|
+
+%%@@@@@@@@@@@@#@@@@@@@@@@@@@%:
|
|
73
|
+
%%%%@@@@@@@@@@%::*@@@@@@@@@%@@=
|
|
74
|
+
-%@%%@@@@@@@@@%:.::*@@@@@%%@@*.
|
|
75
|
+
:%@@%%@@@@@@@%.:%#::*@@%@@@-
|
|
76
|
+
:#@@@%@@@@@@%.:%@%+.-#@@@=
|
|
77
|
+
:+#@@@@@@@%@@@@%.:%@@@%=.+@@@%*-
|
|
78
|
+
.*#%%%%%%@@@@@@@@@@.:%@@@@@*:.+%@@@@#-
|
|
79
|
+
+%%%%%%%%%%@@@@@@@@::##*%* :----%@@@@%.
|
|
80
|
+
.@%%%%%%%%%@@@@@@@@@:.-..-#=:%@@@@@@@@@+
|
|
81
|
+
*@@%%%%%%%%@@@@@@@@@: =#+ +#.-@@@@@@@@@@.
|
|
82
|
+
-@@@%%%%%%%@@@@@@@@@@#%@@@- -.:*@@@@@@@@@*
|
|
83
|
+
%@@@@%@@%@@@@@@@@@@@@@@@@@%**%@@@@@@@@@@@@:
|
|
84
|
+
=@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*`;
|
|
85
|
+
/** Rows the footer (wordmark + version + hint + margins) needs below the art. */
|
|
86
|
+
const FOOTER_ROWS = 5;
|
|
87
|
+
function measure(art) {
|
|
88
|
+
const lines = art.split('\n');
|
|
89
|
+
return {
|
|
90
|
+
art,
|
|
91
|
+
width: Math.max(...lines.map((l) => l.length)),
|
|
92
|
+
height: lines.length,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const VARIANTS = [measure(EMBLEM_LARGE), measure(EMBLEM_SMALL)];
|
|
96
|
+
/**
|
|
97
|
+
* Pure helper: pick the largest emblem variant that fits `columns`×`rows`
|
|
98
|
+
* (leaving room for the wordmark footer), or null when none fits.
|
|
99
|
+
*/
|
|
100
|
+
export function pickSplashArt(columns, rows) {
|
|
101
|
+
for (const v of VARIANTS) {
|
|
102
|
+
if (v.width <= columns - 2 && v.height + FOOTER_ROWS <= rows)
|
|
103
|
+
return v;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Pure helper: should the splash be shown at all?
|
|
109
|
+
* Skipped for non-TTY stdout (pipes/CI), when ZELARI_NO_SPLASH=1, or when
|
|
110
|
+
* the terminal is too small for even the small variant.
|
|
111
|
+
*/
|
|
112
|
+
export function shouldShowSplash(opts) {
|
|
113
|
+
if (!opts.isTTY)
|
|
114
|
+
return false;
|
|
115
|
+
if (opts.env['ZELARI_NO_SPLASH'] === '1')
|
|
116
|
+
return false;
|
|
117
|
+
return pickSplashArt(opts.columns, opts.rows) !== null;
|
|
118
|
+
}
|
|
119
|
+
function Splash({ onDone, version }) {
|
|
120
|
+
const { isRawModeSupported } = useStdin();
|
|
121
|
+
const columns = process.stdout.columns ?? 80;
|
|
122
|
+
const rows = process.stdout.rows ?? 24;
|
|
123
|
+
const picked = pickSplashArt(columns, rows);
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
const t = setTimeout(onDone, SPLASH_DURATION_MS);
|
|
126
|
+
return () => clearTimeout(t);
|
|
127
|
+
}, [onDone]);
|
|
128
|
+
// Any key skips the splash. Guarded: useInput throws without raw mode.
|
|
129
|
+
useInput(() => {
|
|
130
|
+
onDone();
|
|
131
|
+
}, { isActive: isRawModeSupported === true });
|
|
132
|
+
if (!picked)
|
|
133
|
+
return null;
|
|
134
|
+
return (React.createElement(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: columns, height: rows - 1 },
|
|
135
|
+
React.createElement(Text, { color: "cyan" }, picked.art),
|
|
136
|
+
React.createElement(Box, { marginTop: 1 },
|
|
137
|
+
React.createElement(Text, { bold: true, color: "white" }, 'Z E L A R I C O D E')),
|
|
138
|
+
React.createElement(Text, { dimColor: true }, `${version ? `v${version} — ` : ''}N-THEM Studio`),
|
|
139
|
+
React.createElement(Text, { dimColor: true, italic: true }, "press any key to skip")));
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* SplashGate — renders the splash first, then swaps in `children` (the
|
|
143
|
+
* real App). The App therefore mounts only after the splash dismisses,
|
|
144
|
+
* so its <Static> banner and raw-mode input never fight the splash.
|
|
145
|
+
*/
|
|
146
|
+
export function SplashGate({ children, version, }) {
|
|
147
|
+
const [show, setShow] = useState(() => shouldShowSplash({
|
|
148
|
+
isTTY: process.stdout.isTTY === true,
|
|
149
|
+
env: process.env,
|
|
150
|
+
columns: process.stdout.columns ?? 80,
|
|
151
|
+
rows: process.stdout.rows ?? 24,
|
|
152
|
+
}));
|
|
153
|
+
if (show) {
|
|
154
|
+
return React.createElement(Splash, { onDone: () => setShow(false), version: version });
|
|
155
|
+
}
|
|
156
|
+
return React.createElement(React.Fragment, null, children);
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=SplashScreen.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SplashScreen.js","sourceRoot":"","sources":["../../../src/cli/components/SplashScreen.tsx"],"names":[],"mappings":"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"}
|