fix(setup): wizard errors copyable above the dialog, CLI skew explained, exec transparency
Some checks failed
Security / Security check (push) Failing after 2s
Some checks failed
Security / Security check (push) Failing after 2s
Field test of the setup wizard surfaced three trust breaks in one run: an unexplained macOS Documents permission prompt, a perceived crash, and an error message whose copy button could not be reached. Root causes and fixes: - chain init failures were shown as a SnackBar, which lands BEHIND the wizard's modal barrier: dimmed, clipped, copy unreachable — and the click aimed at it hit the barrier, dismissing the whole wizard with all answers (the perceived crash). Errors now open a modal dialog ABOVE the wizard via showChainErrorDialog with a copyable detail block, and the wizard is no longer barrier-dismissible. - When the resolved chain binary is older than Studio and rejects --plan-json, the wizard now explains the version skew in plain language (binary path + update path) instead of leaking a raw clap usage error. A missing binary gets its own localized story. - Step 3 announces which chain binary the preview will execute; when that binary physically lives (symlinks resolved) in a TCC-protected folder, the wizard pre-explains the macOS folder prompt. Supporting changes: FriendlyError passes through friendlyError() unchanged so call sites can ship precise localized stories through the shared presentation; SystemActions gains resolvedChainBinary() plus run/resolve test seams; ChainErrorBox hugs its content instead of filling an unbounded dialog; the wizard's answers file is written synchronously (the async dart:io variants never complete under the widget-test fake-async zone). Verified: flutter analyze clean, 53 tests green (6 new wizard error- path tests incl. clipboard round-trip), plus a live GUI walk on macOS in dark + light with a stale binary (skew dialog, copy verified via clipboard) and with the real binary (TCC pre-explanation with the resolved path, full plan preview). Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
4ceb5bb567
commit
c6da5025ce
11 changed files with 524 additions and 10 deletions
|
|
@ -26,12 +26,27 @@ class FriendlyError {
|
|||
required this.detail,
|
||||
this.hint,
|
||||
});
|
||||
|
||||
// The on-disk error log serialises thrown objects via toString —
|
||||
// keep the whole story (headline, hint, verbatim detail) in one
|
||||
// readable record.
|
||||
@override
|
||||
String toString() => [
|
||||
headline,
|
||||
?hint,
|
||||
if (detail.isNotEmpty && detail != headline) detail,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
/// Map an arbitrary thrown object to a [FriendlyError]. Always
|
||||
/// returns a value — never throws — so callers can drop the
|
||||
/// result straight into UI without try/catch ceremony.
|
||||
FriendlyError friendlyError(Object error, AppLocalizations l) {
|
||||
// A pre-built FriendlyError passes through unchanged — call
|
||||
// sites that already know the precise story (e.g. the setup
|
||||
// wizard's CLI-version-skew case) construct one directly and
|
||||
// still route through the shared presentation helpers.
|
||||
if (error is FriendlyError) return error;
|
||||
// We deliberately don't import package:grpc here so Studio
|
||||
// doesn't have to add it to its own pubspec — the dependency
|
||||
// lives one layer down in chain_client_sdk. `GrpcError` has a
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Sentinel returned in `_runFai(...).stderr` when no `fai`
|
||||
|
|
@ -73,6 +74,28 @@ class SystemActions {
|
|||
/// binary / read the install guide" recovery path.
|
||||
static bool chainBinaryExists() => _faiExecutable() != null;
|
||||
|
||||
/// Test seam: when set, [_runFai] returns this function's result
|
||||
/// instead of spawning a real process, and [resolvedChainBinary] /
|
||||
/// [chainBinaryExists] answer from [debugResolveOverride]. Lets
|
||||
/// widget tests drive the CLI error paths deterministically.
|
||||
@visibleForTesting
|
||||
static Future<({bool ok, String stdout, String stderr})> Function(
|
||||
List<String> args,
|
||||
)?
|
||||
debugRunFaiOverride;
|
||||
|
||||
/// Test seam companion to [debugRunFaiOverride]: overrides binary
|
||||
/// resolution (may return null to simulate "no binary found").
|
||||
@visibleForTesting
|
||||
static String? Function()? debugResolveOverride;
|
||||
|
||||
/// Absolute path of the `chain` binary Studio would execute right
|
||||
/// now, or null when none can be located. Public so surfaces that
|
||||
/// are about to spawn the binary (the guided-setup wizard) can say
|
||||
/// WHAT they will run — before macOS asks the operator for folder
|
||||
/// permission because of where that binary happens to live.
|
||||
static String? resolvedChainBinary() => _faiExecutable();
|
||||
|
||||
/// Ask the OS to open [path] in the default handler. On macOS
|
||||
/// this opens text files in TextEdit, configs in the registered
|
||||
/// editor, etc. Returns true on a clean spawn (process exited
|
||||
|
|
@ -219,6 +242,8 @@ class SystemActions {
|
|||
static Future<({bool ok, String stdout, String stderr})> _runFai(
|
||||
List<String> args,
|
||||
) async {
|
||||
final runOverride = debugRunFaiOverride;
|
||||
if (runOverride != null) return runOverride(args);
|
||||
final exe = _faiExecutable();
|
||||
if (exe == null) {
|
||||
// Sentinel, not a user-facing string. Callers detect this
|
||||
|
|
@ -249,6 +274,8 @@ class SystemActions {
|
|||
/// the file picker), $CHAIN_BIN, PATH, fallback to the canonical
|
||||
/// install location under the user's home dir.
|
||||
static String? _faiExecutable() {
|
||||
final resolveOverride = debugResolveOverride;
|
||||
if (resolveOverride != null) return resolveOverride();
|
||||
final override = _faiBinaryOverride;
|
||||
if (override != null &&
|
||||
override.isNotEmpty &&
|
||||
|
|
|
|||
|
|
@ -1778,6 +1778,20 @@
|
|||
"setupFreeTextPrivacyRemote": "Ihre Beschreibung wird an die eingerichtete System-KI gesendet: {model} ({provider}).",
|
||||
"@setupFreeTextPrivacyRemote": {"placeholders": {"model": {"type": "String"}, "provider": {"type": "String"}}},
|
||||
"setupFreeTextUnavailable": "Für den Freitext-Weg braucht Ch∆In eine eingerichtete System-KI (Einstellungen → System-KI). Die Auswahl oben funktioniert immer — ganz ohne KI.",
|
||||
"setupExecHint": "Für Vorschau und Einrichtung führt Studio das Programm „chain“ aus: {path}",
|
||||
"@setupExecHint": {"placeholders": {"path": {"type": "String"}}},
|
||||
"setupExecHintTcc": "Das Programm liegt unter {path} — macOS fragt dafür eventuell einmalig nach Zugriff auf diesen Ordner (z. B. „Dokumente“). Das ist zu erwarten und in Ordnung.",
|
||||
"@setupExecHintTcc": {"placeholders": {"path": {"type": "String"}}},
|
||||
"setupNoBinaryStepHint": "Das Programm „chain“ wurde auf diesem Rechner nicht gefunden — die Vorschau im nächsten Schritt wird fehlschlagen. Unter „Diagnose“ können Sie es auswählen oder Ch∆In installieren.",
|
||||
"setupNoBinary": "Das Programm „chain“ wurde auf diesem Rechner nicht gefunden.",
|
||||
"setupNoBinaryHint": "Ohne das Programm kann der Assistent die Einrichtung nicht anwenden. Wählen Sie es unter „Diagnose“ aus oder installieren Sie Ch∆In neu.",
|
||||
"setupCliTooOld": "Das installierte „chain“-Programm ist älter als Studio und kennt diesen Assistenten noch nicht.",
|
||||
"setupCliTooOldHint": "Gefunden wurde: {path}. Bitte Ch∆In aktualisieren (Seite „Diagnose“ → Update, oder im Terminal: chain update apply) und den Assistenten danach erneut öffnen.",
|
||||
"@setupCliTooOldHint": {"placeholders": {"path": {"type": "String"}}},
|
||||
"setupPreviewFailed": "Die Plan-Vorschau konnte nicht erstellt werden — das Programm „chain“ meldet einen Fehler.",
|
||||
"setupApplyFailed": "Die Einrichtung konnte nicht angewendet werden — das Programm „chain“ meldet einen Fehler.",
|
||||
"setupCliUsedBinary": "Ausgeführt wurde: {path}",
|
||||
"@setupCliUsedBinary": {"placeholders": {"path": {"type": "String"}}},
|
||||
"setupFreeTextParseError": "Die System-KI hat keinen verwertbaren Vorschlag geliefert. Wählen Sie oben aus dem Menü — oder versuchen Sie es noch einmal.",
|
||||
"setupReflectionTitle": "So verstehe ich Ihre Aufgabe",
|
||||
"setupReflectionScenario": "Worum es geht: {label}",
|
||||
|
|
|
|||
|
|
@ -1817,6 +1817,20 @@
|
|||
"setupFreeTextPrivacyRemote": "Your description is sent to the configured system AI: {model} ({provider}).",
|
||||
"@setupFreeTextPrivacyRemote": {"placeholders": {"model": {"type": "String"}, "provider": {"type": "String"}}},
|
||||
"setupFreeTextUnavailable": "The free-text path needs a configured system AI (Settings → System AI). The choices above always work — no AI required.",
|
||||
"setupExecHint": "For the preview and setup, Studio runs the “chain” program: {path}",
|
||||
"@setupExecHint": {"placeholders": {"path": {"type": "String"}}},
|
||||
"setupExecHintTcc": "The program lives at {path} — macOS may ask once for access to that folder (e.g. “Documents”). That is expected and fine.",
|
||||
"@setupExecHintTcc": {"placeholders": {"path": {"type": "String"}}},
|
||||
"setupNoBinaryStepHint": "The “chain” program could not be found on this machine — the preview in the next step will fail. You can pick it under “Doctor”, or install Ch∆In.",
|
||||
"setupNoBinary": "The “chain” program could not be found on this machine.",
|
||||
"setupNoBinaryHint": "Without it the assistant cannot apply the setup. Pick it under “Doctor”, or reinstall Ch∆In.",
|
||||
"setupCliTooOld": "The installed “chain” program is older than Studio and does not know this assistant yet.",
|
||||
"setupCliTooOldHint": "Found: {path}. Please update Ch∆In (“Doctor” page → update, or in a terminal: chain update apply) and reopen the assistant afterwards.",
|
||||
"@setupCliTooOldHint": {"placeholders": {"path": {"type": "String"}}},
|
||||
"setupPreviewFailed": "The plan preview could not be created — the “chain” program reported an error.",
|
||||
"setupApplyFailed": "The setup could not be applied — the “chain” program reported an error.",
|
||||
"setupCliUsedBinary": "Executed: {path}",
|
||||
"@setupCliUsedBinary": {"placeholders": {"path": {"type": "String"}}},
|
||||
"setupFreeTextParseError": "The system AI did not return a usable suggestion. Pick from the menu above — or try again.",
|
||||
"setupReflectionTitle": "This is how I read your task",
|
||||
"setupReflectionScenario": "What it is about: {label}",
|
||||
|
|
|
|||
|
|
@ -5479,6 +5479,66 @@ abstract class AppLocalizations {
|
|||
/// **'The free-text path needs a configured system AI (Settings → System AI). The choices above always work — no AI required.'**
|
||||
String get setupFreeTextUnavailable;
|
||||
|
||||
/// No description provided for @setupExecHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'For the preview and setup, Studio runs the “chain” program: {path}'**
|
||||
String setupExecHint(String path);
|
||||
|
||||
/// No description provided for @setupExecHintTcc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The program lives at {path} — macOS may ask once for access to that folder (e.g. “Documents”). That is expected and fine.'**
|
||||
String setupExecHintTcc(String path);
|
||||
|
||||
/// No description provided for @setupNoBinaryStepHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The “chain” program could not be found on this machine — the preview in the next step will fail. You can pick it under “Doctor”, or install Ch∆In.'**
|
||||
String get setupNoBinaryStepHint;
|
||||
|
||||
/// No description provided for @setupNoBinary.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The “chain” program could not be found on this machine.'**
|
||||
String get setupNoBinary;
|
||||
|
||||
/// No description provided for @setupNoBinaryHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Without it the assistant cannot apply the setup. Pick it under “Doctor”, or reinstall Ch∆In.'**
|
||||
String get setupNoBinaryHint;
|
||||
|
||||
/// No description provided for @setupCliTooOld.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The installed “chain” program is older than Studio and does not know this assistant yet.'**
|
||||
String get setupCliTooOld;
|
||||
|
||||
/// No description provided for @setupCliTooOldHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Found: {path}. Please update Ch∆In (“Doctor” page → update, or in a terminal: chain update apply) and reopen the assistant afterwards.'**
|
||||
String setupCliTooOldHint(String path);
|
||||
|
||||
/// No description provided for @setupPreviewFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The plan preview could not be created — the “chain” program reported an error.'**
|
||||
String get setupPreviewFailed;
|
||||
|
||||
/// No description provided for @setupApplyFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The setup could not be applied — the “chain” program reported an error.'**
|
||||
String get setupApplyFailed;
|
||||
|
||||
/// No description provided for @setupCliUsedBinary.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Executed: {path}'**
|
||||
String setupCliUsedBinary(String path);
|
||||
|
||||
/// No description provided for @setupFreeTextParseError.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -3238,6 +3238,50 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get setupFreeTextUnavailable =>
|
||||
'Für den Freitext-Weg braucht Ch∆In eine eingerichtete System-KI (Einstellungen → System-KI). Die Auswahl oben funktioniert immer — ganz ohne KI.';
|
||||
|
||||
@override
|
||||
String setupExecHint(String path) {
|
||||
return 'Für Vorschau und Einrichtung führt Studio das Programm „chain“ aus: $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String setupExecHintTcc(String path) {
|
||||
return 'Das Programm liegt unter $path — macOS fragt dafür eventuell einmalig nach Zugriff auf diesen Ordner (z. B. „Dokumente“). Das ist zu erwarten und in Ordnung.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get setupNoBinaryStepHint =>
|
||||
'Das Programm „chain“ wurde auf diesem Rechner nicht gefunden — die Vorschau im nächsten Schritt wird fehlschlagen. Unter „Diagnose“ können Sie es auswählen oder Ch∆In installieren.';
|
||||
|
||||
@override
|
||||
String get setupNoBinary =>
|
||||
'Das Programm „chain“ wurde auf diesem Rechner nicht gefunden.';
|
||||
|
||||
@override
|
||||
String get setupNoBinaryHint =>
|
||||
'Ohne das Programm kann der Assistent die Einrichtung nicht anwenden. Wählen Sie es unter „Diagnose“ aus oder installieren Sie Ch∆In neu.';
|
||||
|
||||
@override
|
||||
String get setupCliTooOld =>
|
||||
'Das installierte „chain“-Programm ist älter als Studio und kennt diesen Assistenten noch nicht.';
|
||||
|
||||
@override
|
||||
String setupCliTooOldHint(String path) {
|
||||
return 'Gefunden wurde: $path. Bitte Ch∆In aktualisieren (Seite „Diagnose“ → Update, oder im Terminal: chain update apply) und den Assistenten danach erneut öffnen.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get setupPreviewFailed =>
|
||||
'Die Plan-Vorschau konnte nicht erstellt werden — das Programm „chain“ meldet einen Fehler.';
|
||||
|
||||
@override
|
||||
String get setupApplyFailed =>
|
||||
'Die Einrichtung konnte nicht angewendet werden — das Programm „chain“ meldet einen Fehler.';
|
||||
|
||||
@override
|
||||
String setupCliUsedBinary(String path) {
|
||||
return 'Ausgeführt wurde: $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get setupFreeTextParseError =>
|
||||
'Die System-KI hat keinen verwertbaren Vorschlag geliefert. Wählen Sie oben aus dem Menü — oder versuchen Sie es noch einmal.';
|
||||
|
|
|
|||
|
|
@ -3234,6 +3234,50 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get setupFreeTextUnavailable =>
|
||||
'The free-text path needs a configured system AI (Settings → System AI). The choices above always work — no AI required.';
|
||||
|
||||
@override
|
||||
String setupExecHint(String path) {
|
||||
return 'For the preview and setup, Studio runs the “chain” program: $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String setupExecHintTcc(String path) {
|
||||
return 'The program lives at $path — macOS may ask once for access to that folder (e.g. “Documents”). That is expected and fine.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get setupNoBinaryStepHint =>
|
||||
'The “chain” program could not be found on this machine — the preview in the next step will fail. You can pick it under “Doctor”, or install Ch∆In.';
|
||||
|
||||
@override
|
||||
String get setupNoBinary =>
|
||||
'The “chain” program could not be found on this machine.';
|
||||
|
||||
@override
|
||||
String get setupNoBinaryHint =>
|
||||
'Without it the assistant cannot apply the setup. Pick it under “Doctor”, or reinstall Ch∆In.';
|
||||
|
||||
@override
|
||||
String get setupCliTooOld =>
|
||||
'The installed “chain” program is older than Studio and does not know this assistant yet.';
|
||||
|
||||
@override
|
||||
String setupCliTooOldHint(String path) {
|
||||
return 'Found: $path. Please update Ch∆In (“Doctor” page → update, or in a terminal: chain update apply) and reopen the assistant afterwards.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get setupPreviewFailed =>
|
||||
'The plan preview could not be created — the “chain” program reported an error.';
|
||||
|
||||
@override
|
||||
String get setupApplyFailed =>
|
||||
'The setup could not be applied — the “chain” program reported an error.';
|
||||
|
||||
@override
|
||||
String setupCliUsedBinary(String path) {
|
||||
return 'Executed: $path';
|
||||
}
|
||||
|
||||
@override
|
||||
String get setupFreeTextParseError =>
|
||||
'The system AI did not return a usable suggestion. Pick from the menu above — or try again.';
|
||||
|
|
|
|||
|
|
@ -97,6 +97,10 @@ class _ChainErrorBoxState extends State<ChainErrorBox> {
|
|||
border: Border.all(color: accent.withValues(alpha: 0.4)),
|
||||
),
|
||||
child: Column(
|
||||
// min, not max: inside an unbounded-height dialog the box
|
||||
// must hug its content — otherwise a one-line error renders
|
||||
// as a screen-tall empty red frame.
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Align(
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import 'dart:io';
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../data/error_presentation.dart';
|
||||
import '../data/friendly_error.dart';
|
||||
import '../data/hub.dart';
|
||||
import '../data/system_actions.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
|
@ -118,8 +119,14 @@ class GuidedSetupDialog extends StatefulWidget {
|
|||
|
||||
static Future<void> show(BuildContext context) {
|
||||
final shell = StudioShellState.of(context);
|
||||
// Not barrier-dismissible: a stray click outside the dialog must
|
||||
// not throw away a half-answered wizard (and error SnackBars used
|
||||
// to lure exactly that click — the operator aimed for the message
|
||||
// below the barrier and lost everything). Leaving is explicit:
|
||||
// Abbrechen on step 1, Zurück everywhere else.
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => GuidedSetupDialog(shell: shell),
|
||||
);
|
||||
}
|
||||
|
|
@ -276,12 +283,15 @@ class _GuidedSetupDialogState extends State<GuidedSetupDialog> {
|
|||
/// racy on multi-user machines. Cleaned up in [dispose].
|
||||
Directory? _answersDir;
|
||||
|
||||
Future<String> _writeAnswers() async {
|
||||
_answersDir ??= await Directory.systemTemp.createTemp('chain-setup-');
|
||||
String _writeAnswers() {
|
||||
// Sync on purpose: the file is a handful of lines, and the async
|
||||
// dart:io variants never complete inside the fake-async zone
|
||||
// widget tests run in — the wizard would hang there forever.
|
||||
_answersDir ??= Directory.systemTemp.createTempSync('chain-setup-');
|
||||
final f = File(
|
||||
'${_answersDir!.path}${Platform.pathSeparator}answers.yaml',
|
||||
);
|
||||
await f.writeAsString(_answersYaml());
|
||||
f.writeAsStringSync(_answersYaml());
|
||||
return f.path;
|
||||
}
|
||||
|
||||
|
|
@ -299,7 +309,7 @@ class _GuidedSetupDialogState extends State<GuidedSetupDialog> {
|
|||
_busy = true;
|
||||
_plan = null;
|
||||
});
|
||||
final path = await _writeAnswers();
|
||||
final path = _writeAnswers();
|
||||
final r = await SystemActions.chainInit(['--answers', path, '--plan-json']);
|
||||
if (!mounted) return;
|
||||
setState(() => _busy = false);
|
||||
|
|
@ -311,13 +321,85 @@ class _GuidedSetupDialogState extends State<GuidedSetupDialog> {
|
|||
_step = _totalSteps; // review
|
||||
});
|
||||
} catch (_) {
|
||||
showFaiProcessError(context, 'chain init --plan-json', r.stdout, r.stderr);
|
||||
await _showCliError('chain init --plan-json', r.stdout, r.stderr);
|
||||
}
|
||||
} else {
|
||||
showFaiProcessError(context, 'chain init --plan-json', r.stdout, r.stderr);
|
||||
await _showCliError('chain init --plan-json', r.stdout, r.stderr);
|
||||
}
|
||||
}
|
||||
|
||||
/// Surface a failed `chain init` call as a modal dialog stacked
|
||||
/// ABOVE this wizard. A SnackBar would land BEHIND the wizard's
|
||||
/// modal barrier: visible but dimmed, its copy button unreachable —
|
||||
/// and the operator's attempt to click it hits the barrier instead.
|
||||
/// Known failure shapes get a localized plain-language headline;
|
||||
/// the verbatim CLI output stays copyable behind "Details".
|
||||
Future<void> _showCliError(String source, String stdout, String stderr) {
|
||||
final l = AppLocalizations.of(context)!;
|
||||
final raw = [
|
||||
stderr.trim(),
|
||||
stdout.trim(),
|
||||
].where((s) => s.isNotEmpty).join('\n\n');
|
||||
final bin = SystemActions.resolvedChainBinary();
|
||||
final FriendlyError friendly;
|
||||
if (stderr.trim() == kFaiBinaryNotFound) {
|
||||
// Sentinel, never shown verbatim (see SystemActions docs).
|
||||
friendly = FriendlyError(
|
||||
headline: l.setupNoBinary,
|
||||
detail: '',
|
||||
hint: l.setupNoBinaryHint,
|
||||
);
|
||||
} else if (raw.contains('unexpected argument')) {
|
||||
// Version skew: this Studio speaks a newer `chain init` dialect
|
||||
// than the binary it found (e.g. a stale build behind a channel
|
||||
// symlink). Name the binary and the way out instead of leaking
|
||||
// a raw clap usage error.
|
||||
friendly = FriendlyError(
|
||||
headline: l.setupCliTooOld,
|
||||
detail: raw,
|
||||
hint: l.setupCliTooOldHint(bin ?? 'chain'),
|
||||
);
|
||||
} else {
|
||||
friendly = FriendlyError(
|
||||
headline: source == 'chain init --apply'
|
||||
? l.setupApplyFailed
|
||||
: l.setupPreviewFailed,
|
||||
detail: raw.isEmpty ? 'process exited non-zero (no output)' : raw,
|
||||
hint: bin == null ? null : l.setupCliUsedBinary(bin),
|
||||
);
|
||||
}
|
||||
return showChainErrorDialog(context, source, friendly);
|
||||
}
|
||||
|
||||
/// Physical location of the resolved `chain` binary (symlinks
|
||||
/// followed) — the path that decides whether macOS shows a folder-
|
||||
/// permission prompt when Studio executes it, regardless of how
|
||||
/// harmless the symlink's own path looks.
|
||||
String? _physicalChainBinary() {
|
||||
final bin = SystemActions.resolvedChainBinary();
|
||||
if (bin == null) return null;
|
||||
try {
|
||||
return File(bin).resolveSymbolicLinksSync();
|
||||
} on FileSystemException {
|
||||
return bin;
|
||||
}
|
||||
}
|
||||
|
||||
/// True when executing the resolved binary can trigger a macOS
|
||||
/// folder-permission (TCC) prompt because it physically lives in a
|
||||
/// protected folder. The wizard says so BEFORE the first exec —
|
||||
/// an unexplained "Studio wants access to Documents" prompt in the
|
||||
/// middle of setup reads as a trust break.
|
||||
bool _binaryNeedsFolderPermission() {
|
||||
if (!Platform.isMacOS) return false;
|
||||
final physical = _physicalChainBinary();
|
||||
final home = Platform.environment['HOME'];
|
||||
if (physical == null || home == null || home.isEmpty) return false;
|
||||
return physical.startsWith('$home/Documents/') ||
|
||||
physical.startsWith('$home/Desktop/') ||
|
||||
physical.startsWith('$home/Downloads/');
|
||||
}
|
||||
|
||||
/// Warning lines the apply emitted on success (e.g. the empty
|
||||
/// trusted_publishers caveat). Swallowing them made the wizard
|
||||
/// claim more than the config delivers — show them instead.
|
||||
|
|
@ -325,7 +407,7 @@ class _GuidedSetupDialogState extends State<GuidedSetupDialog> {
|
|||
|
||||
Future<void> _apply() async {
|
||||
setState(() => _busy = true);
|
||||
final path = await _writeAnswers();
|
||||
final path = _writeAnswers();
|
||||
final r = await SystemActions.chainInit(
|
||||
['--answers', path, '--apply', '--force'],
|
||||
);
|
||||
|
|
@ -343,7 +425,7 @@ class _GuidedSetupDialogState extends State<GuidedSetupDialog> {
|
|||
});
|
||||
unawaited(_probeHub());
|
||||
} else {
|
||||
showFaiProcessError(context, 'chain init --apply', r.stdout, r.stderr);
|
||||
await _showCliError('chain init --apply', r.stdout, r.stderr);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -527,6 +609,18 @@ class _GuidedSetupDialogState extends State<GuidedSetupDialog> {
|
|||
value: _dataLocal,
|
||||
onChanged: (v) => setState(() => _dataLocal = v),
|
||||
),
|
||||
// Exec transparency: the next click runs the `chain` binary.
|
||||
// Say which one — and warn when its physical location will
|
||||
// make macOS ask for folder access, so the prompt (if any)
|
||||
// arrives explained instead of as a trust break.
|
||||
const SizedBox(height: ChainSpace.sm),
|
||||
if (!SystemActions.chainBinaryExists())
|
||||
_hintRow(l.setupNoBinaryStepHint)
|
||||
else ...[
|
||||
_hintRow(l.setupExecHint(SystemActions.resolvedChainBinary()!)),
|
||||
if (_binaryNeedsFolderPermission())
|
||||
_hintRow(l.setupExecHintTcc(_physicalChainBinary()!)),
|
||||
],
|
||||
],
|
||||
// The free-text alternative lives on the first step: describe
|
||||
// the goal, the system AI pre-selects the menu answers. Menu
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue