Add Project Home, Auth Bridge, shared auth token, and Tier-1 polish
Project Home (DESIGN-REVIEW §B2): the project is promoted from a 280px
sidebar card to a first-class main-area view. ProjectCard.tsx (1,257
lines) is replaced by a select-only ProjectRow plus tabs for Overview,
Sessions, Automation, Config and Files. The PortMappings, FileManager
and ContainerProgress modals are absorbed rather than reimplemented.
Config gains a Saved/Saving/Failed indicator — save-on-blur failures
previously reached only console.error.
Tier-1 polish (DESIGN-REVIEW §A): new elevation, muted-accent, disabled
and focus-ring tokens; a global :focus-visible ring with every
focus:outline-none removed; filled buttons moved to --accent-emphasis
and white-on-success toggles retired, fixing three WCAG AA failures
(2.1:1, 2.5:1, 2.4:1); a shared Modal primitive with role="dialog",
focus trap and restore, adopted by all remaining modals; status
indicators that carry a glyph and word rather than colour alone.
Ctrl+Shift+W closes a tab, deliberately not Ctrl+W — that is readline's
kill-word, used constantly in the terminal this app is built around.
Auth Bridge: a general loopback-callback bridge so browser logins run
inside a container (aws sso login, Concourse fly login, claude login)
can complete against the host browser. Listeners are discovered from
/proc/net/tcp{,6} — ss/netstat/lsof are absent from the image — bound on
host 127.0.0.1 only, and tunnelled in over the Docker API via socat,
which keeps working on Docker Desktop where container IPs are not
routable. Falls back to [::1] because Node resolves localhost to IPv6
first, so claude login often binds ::1 alone. Opt-in per project.
This extracts create_attached_exec() and moves the existing terminal
session path onto it, so there is one attached-exec implementation
rather than two.
Shared auth token: `claude setup-token` is run in a container, the token
is stored in the OS keychain and injected as CLAUDE_CODE_OAUTH_TOKEN
into Anthropic-backend projects. Contrary to the initial design note,
setup-token uses an Anthropic-hosted redirect and blocks on a stdin
paste prompt rather than a loopback callback, so a stdin command is
required for the flow to complete.
The token is never logged, never returned to the frontend, and is
redacted from the streamed output with a stateful matcher that withholds
any tail that could still grow into a secret. Change detection uses a
random rotation id rather than a hash, since a hash in a docker-inspect
readable label would be an offline verification oracle.
Frontend 33 -> 51 tests; Rust 34 tests. Both builds clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import { useInstallHelper } from "../hooks/useInstallHelper";
|
||||
import { useDocker } from "../hooks/useDocker";
|
||||
import Modal from "./ui/Modal";
|
||||
import Button from "./ui/Button";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
@@ -16,27 +18,11 @@ export default function DockerInstallDialog({ onClose }: Props) {
|
||||
const [phase, setPhase] = useState<Phase>("idle");
|
||||
const [log, setLog] = useState<string[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const overlayRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadOptions();
|
||||
}, [loadOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && phase !== "installing") onClose();
|
||||
};
|
||||
document.addEventListener("keydown", onKey);
|
||||
return () => document.removeEventListener("keydown", onKey);
|
||||
}, [onClose, phase]);
|
||||
|
||||
const handleOverlayClick = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (e.target === overlayRef.current && phase !== "installing") onClose();
|
||||
},
|
||||
[onClose, phase],
|
||||
);
|
||||
|
||||
const handleInstall = async () => {
|
||||
setPhase("installing");
|
||||
setLog([]);
|
||||
@@ -70,142 +56,122 @@ export default function DockerInstallDialog({ onClose }: Props) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const installVerb = phase === "installing" ? "Installing…" : `Install ${options.product_name}`;
|
||||
const installVerb =
|
||||
phase === "installing" ? "Installing…" : `Install ${options.product_name}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={overlayRef}
|
||||
onClick={handleOverlayClick}
|
||||
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
||||
<Modal
|
||||
title="Docker not detected"
|
||||
onClose={onClose}
|
||||
widthClassName="w-[34rem]"
|
||||
// Closing mid-install would orphan a privileged installer.
|
||||
dismissible={phase !== "installing"}
|
||||
footer={
|
||||
phase === "idle" ? (
|
||||
<Button variant="ghost" onClick={onClose}>
|
||||
Dismiss
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg p-6 w-[32rem] max-h-[85vh] overflow-y-auto shadow-xl">
|
||||
<h2 className="text-lg font-semibold mb-1">Docker not detected</h2>
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-4">
|
||||
Triple-C needs a Docker-compatible runtime to manage sandboxed project containers.
|
||||
We can install <span className="text-[var(--text-primary)]">{options.product_name}</span>{" "}
|
||||
for you, or you can follow the official instructions.
|
||||
</p>
|
||||
<p className="text-[13px] text-[var(--text-secondary)] mb-4">
|
||||
Triple-C needs a Docker-compatible runtime to manage sandboxed project
|
||||
containers. We can install{" "}
|
||||
<span className="text-[var(--text-primary)]">{options.product_name}</span> for
|
||||
you, or you can follow the official instructions.
|
||||
</p>
|
||||
|
||||
{phase === "idle" && (
|
||||
<div className="flex flex-col gap-2">
|
||||
{options.can_auto_install ? (
|
||||
<button
|
||||
onClick={handleInstall}
|
||||
className="px-3 py-2 text-sm bg-[var(--accent)] text-white rounded hover:bg-[var(--accent-hover)] transition-colors"
|
||||
>
|
||||
{installVerb} ({options.auto_install_method})
|
||||
</button>
|
||||
) : (
|
||||
<div className="text-xs text-[var(--text-secondary)] bg-[var(--bg-primary)] border border-[var(--border-color)] rounded p-2">
|
||||
One-click install unavailable:{" "}
|
||||
<span className="text-[var(--text-primary)]">
|
||||
{options.auto_install_blocker ?? "required tooling missing."}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => setShowManual((s) => !s)}
|
||||
className="px-3 py-2 text-sm bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded hover:bg-[var(--border-color)] transition-colors"
|
||||
>
|
||||
{showManual ? "Hide manual instructions" : "Show manual instructions"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleOpenDocs}
|
||||
className="px-3 py-2 text-sm bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded hover:bg-[var(--border-color)] transition-colors"
|
||||
>
|
||||
Open official documentation ↗
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{phase === "installing" && (
|
||||
<div className="text-xs text-[var(--text-secondary)]">
|
||||
Installing… a system password prompt may appear. Do not close this window.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{phase === "done" && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-sm text-[var(--success)]">Install finished.</div>
|
||||
{options.post_install_notes.length > 0 && (
|
||||
<ul className="text-xs text-[var(--text-secondary)] list-disc list-inside space-y-1">
|
||||
{options.post_install_notes.map((note, i) => (
|
||||
<li key={i}>{note}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<div className="flex gap-2 mt-2">
|
||||
<button
|
||||
onClick={handleRecheck}
|
||||
className="px-3 py-2 text-sm bg-[var(--accent)] text-white rounded hover:bg-[var(--accent-hover)] transition-colors"
|
||||
>
|
||||
Re-check Docker
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-3 py-2 text-sm bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded hover:bg-[var(--border-color)] transition-colors"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
{phase === "idle" && (
|
||||
<div className="flex flex-col gap-2">
|
||||
{options.can_auto_install ? (
|
||||
<Button size="md" variant="primary" onClick={handleInstall}>
|
||||
{installVerb} ({options.auto_install_method})
|
||||
</Button>
|
||||
) : (
|
||||
<div className="text-xs text-[var(--text-secondary)] bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-[var(--radius-control)] p-2">
|
||||
One-click install unavailable:{" "}
|
||||
<span className="text-[var(--text-primary)]">
|
||||
{options.auto_install_blocker ?? "required tooling missing."}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{phase === "error" && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-sm text-[var(--error)]">Install failed.</div>
|
||||
{error && <div className="text-xs font-mono text-[var(--error)]">{error}</div>}
|
||||
<div className="flex gap-2 mt-2">
|
||||
<button
|
||||
onClick={() => setPhase("idle")}
|
||||
className="px-3 py-2 text-sm bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded hover:bg-[var(--border-color)] transition-colors"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
onClick={handleOpenDocs}
|
||||
className="px-3 py-2 text-sm bg-[var(--accent)] text-white rounded hover:bg-[var(--accent-hover)] transition-colors"
|
||||
>
|
||||
Open official docs ↗
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Button size="md" onClick={() => setShowManual((s) => !s)}>
|
||||
{showManual ? "Hide manual instructions" : "Show manual instructions"}
|
||||
</Button>
|
||||
|
||||
{(showManual || phase === "error") && (
|
||||
<div className="mt-4">
|
||||
<div className="text-xs font-medium mb-1.5 text-[var(--text-secondary)]">
|
||||
Manual install steps
|
||||
</div>
|
||||
<ol className="text-xs text-[var(--text-secondary)] list-decimal list-inside space-y-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded p-2">
|
||||
{options.manual_steps.map((step, i) => (
|
||||
<li key={i}>{step}</li>
|
||||
<Button size="md" onClick={handleOpenDocs}>
|
||||
Open official documentation ↗
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{phase === "installing" && (
|
||||
<div className="text-xs text-[var(--text-secondary)]">
|
||||
Installing… a system password prompt may appear. Do not close this window.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{phase === "done" && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-[13px] text-[var(--success)]">Install finished.</div>
|
||||
{options.post_install_notes.length > 0 && (
|
||||
<ul className="text-xs text-[var(--text-secondary)] list-disc list-inside space-y-1">
|
||||
{options.post_install_notes.map((note, i) => (
|
||||
<li key={i}>{note}</li>
|
||||
))}
|
||||
</ol>
|
||||
</ul>
|
||||
)}
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Button size="md" variant="primary" onClick={handleRecheck}>
|
||||
Re-check Docker
|
||||
</Button>
|
||||
<Button size="md" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{log.length > 0 && (
|
||||
<div className="mt-4 max-h-48 overflow-y-auto bg-[var(--bg-primary)] border border-[var(--border-color)] rounded p-2 text-xs font-mono text-[var(--text-secondary)]">
|
||||
{log.map((line, i) => (
|
||||
<div key={i}>{line}</div>
|
||||
{phase === "error" && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-[13px] text-[var(--error)]">Install failed.</div>
|
||||
{error && (
|
||||
<div className="text-xs font-mono text-[var(--error)] break-words">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Button size="md" onClick={() => setPhase("idle")}>
|
||||
Back
|
||||
</Button>
|
||||
<Button size="md" variant="primary" onClick={handleOpenDocs}>
|
||||
Open official docs ↗
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(showManual || phase === "error") && (
|
||||
<div className="mt-4">
|
||||
<div className="text-xs font-medium mb-1.5 text-[var(--text-secondary)]">
|
||||
Manual install steps
|
||||
</div>
|
||||
<ol className="text-xs text-[var(--text-secondary)] list-decimal list-inside space-y-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-[var(--radius-control)] p-2">
|
||||
{options.manual_steps.map((step, i) => (
|
||||
<li key={i}>{step}</li>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ol>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{phase === "idle" && (
|
||||
<div className="mt-4 flex justify-end">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{log.length > 0 && (
|
||||
<div className="mt-4 max-h-48 overflow-y-auto bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-[var(--radius-control)] p-2 text-xs font-mono text-[var(--text-secondary)]">
|
||||
{log.map((line, i) => (
|
||||
<div key={i}>{line}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user