diff --git a/src-dotnet/PuduLauncher/Services/TtsInstallService.cs b/src-dotnet/PuduLauncher/Services/TtsInstallService.cs
index 6cb5625..eaa0907 100644
--- a/src-dotnet/PuduLauncher/Services/TtsInstallService.cs
+++ b/src-dotnet/PuduLauncher/Services/TtsInstallService.cs
@@ -39,6 +39,13 @@ public async Task RunInstallerAsync(string extractDir, string installPath, Cance
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
+ // Redirect stdin so the installer (and the bundled Python it spawns for
+ // the python env step) gets a private handle instead of inheriting the
+ // sidecar's stdin, a pipe the Rust host owns for the ACK/SHUTDOWN signal.
+ // The bundled Python blocks while initializing its standard streams on
+ // that inherited pipe, hanging the install. See TtsServerService for the
+ // same fix on the server process.
+ RedirectStandardInput = true,
};
using var process = new Process();
@@ -46,6 +53,11 @@ public async Task RunInstallerAsync(string extractDir, string installPath, Cance
process.EnableRaisingEvents = true;
process.Start();
+ // The installer never reads stdin; close it so the child sees EOF rather
+ // than an open idle pipe. The redirect itself is what keeps it off the
+ // sidecar's inherited stdin (see ProcessStartInfo above).
+ process.StandardInput.Close();
+
lock (_processLock)
{
_currentInstallerProcess = process;
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 55d3720..9a99154 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "pudu-launcher",
- "version": "2.0.1",
+ "version": "2.0.2",
"identifier": "com.corp0.pudu-launcher",
"build": {
"beforeDevCommand": "npm run generate-ts && npm run build-sidecar && npm run dev",
diff --git a/src/components/layouts/tts/TtsInstallerLayout.tsx b/src/components/layouts/tts/TtsInstallerLayout.tsx
index bc3117b..b33d784 100644
--- a/src/components/layouts/tts/TtsInstallerLayout.tsx
+++ b/src/components/layouts/tts/TtsInstallerLayout.tsx
@@ -56,6 +56,7 @@ export default function TtsInstallerLayout(props: TtsInstallerLayoutProps) {
currentStep={currentStep}
stepLabels={stepLabels}
isComplete={isComplete}
+ startNumber={0}
/>
{(statusMessage || statusLabel) && (
diff --git a/src/components/organisms/common/PuduStepper.tsx b/src/components/organisms/common/PuduStepper.tsx
index 15355b6..0772b09 100644
--- a/src/components/organisms/common/PuduStepper.tsx
+++ b/src/components/organisms/common/PuduStepper.tsx
@@ -6,10 +6,14 @@ interface PuduStepperProps {
currentStep: number;
stepLabels?: string[];
isComplete?: boolean;
+ // Number shown on the first indicator. Defaults to 1. Set to 0 to align the
+ // displayed numbers with externally numbered phases (for example installer
+ // log lines that count from a zero-based "prepare" step).
+ startNumber?: number;
}
export default function PuduStepper(props: PuduStepperProps) {
- const { maxSteps, currentStep, stepLabels, isComplete = false } = props;
+ const { maxSteps, currentStep, stepLabels, isComplete = false, startNumber = 1 } = props;
const stepCount = Math.max(1, Math.floor(maxSteps));
const activeStep = Math.min(Math.max(Math.floor(currentStep), 1), stepCount);
@@ -32,7 +36,7 @@ export default function PuduStepper(props: PuduStepperProps) {
variant={indicatorVariant}
color={indicatorColor}
>
- {isCompleted ? : stepNumber}
+ {isCompleted ? : stepNumber - 1 + startNumber}
}
>
diff --git a/src/contextProviders/TtsInstallerContextProvider.tsx b/src/contextProviders/TtsInstallerContextProvider.tsx
index b56041c..e859972 100644
--- a/src/contextProviders/TtsInstallerContextProvider.tsx
+++ b/src/contextProviders/TtsInstallerContextProvider.tsx
@@ -85,10 +85,8 @@ export function TtsInstallerContextProvider(props: PropsWithChildren) {
const { ttsState, status, statusMessage, installLogs, clearInstallLogs } = useTtsState();
const [isInstallerOpen, setIsInstallerOpen] = useState(false);
- const [maxReachedStep, setMaxReachedStep] = useState(1);
const installSessionRef = useRef(false);
const prevStatusRef = useRef(null);
- const prevLogLengthRef = useRef(0);
const prevUpdateAvailableRef = useRef(false);
const beginInstallSession = () => {
@@ -98,7 +96,6 @@ export function TtsInstallerContextProvider(props: PropsWithChildren) {
installSessionRef.current = true;
clearInstallLogs();
- setMaxReachedStep(1);
};
// React to status changes
@@ -132,25 +129,18 @@ export function TtsInstallerContextProvider(props: PropsWithChildren) {
showInfo({ message: "TTS server stopped" });
}
- setMaxReachedStep((prev) => Math.max(prev, stepFromStatus(status)));
prevStatusRef.current = status;
}, [status]);
- // React to new install log lines
+ // React to new install log lines. This effect only opens the installer modal
+ // when logs start streaming. The active step is derived from installLogs
+ // directly (see currentStep below) rather than tracked incrementally, so it
+ // can never desync from the log contents.
useEffect(() => {
if (installLogs.length > 0 && !installSessionRef.current) {
beginInstallSession();
setIsInstallerOpen(true);
}
-
- for (let i = prevLogLengthRef.current; i < installLogs.length; i++) {
- const parsed = inferStepFromLogLine(installLogs[i]);
- if (parsed !== null) {
- setMaxReachedStep((prev) => Math.max(prev, parsed));
- }
- }
-
- prevLogLengthRef.current = installLogs.length;
}, [installLogs]);
useEffect(() => {
@@ -184,7 +174,20 @@ export function TtsInstallerContextProvider(props: PropsWithChildren) {
const statusLabel = status !== null
? (TTS_STATUS_LABELS[status] ?? `Status ${status}`)
: "Unknown";
- const currentStep = maxReachedStep;
+ // Derive the active step from the log contents on every render instead of
+ // tracking it incrementally. An incremental index cursor desynced from
+ // installLogs whenever the shared log array was cleared for a new session,
+ // skipping step markers and freezing the stepper (for example showing
+ // "Python" while packages were already installing). Re-scanning is cheap
+ // (logs are capped at 400 lines) and cannot drift from what the log shows.
+ const stepFromLogs = installLogs.reduce((maxStep, line) => {
+ const parsed = inferStepFromLogLine(line);
+ return parsed === null ? maxStep : Math.max(maxStep, parsed);
+ }, 1);
+ const currentStep = Math.min(
+ Math.max(stepFromLogs, stepFromStatus(status)),
+ INSTALL_STEP_LABELS.length,
+ );
const isInstallComplete = status === TTS_STATUS.Installed;
const stepStatusMessage = isInstallComplete
? INSTALL_SUCCESS_MESSAGE