feat(run): per-tool run setup fields and binary probe
This commit is contained in:
@@ -28,8 +28,10 @@ import type {
|
|||||||
RunHandle,
|
RunHandle,
|
||||||
RunListResponse,
|
RunListResponse,
|
||||||
RunStartArgs,
|
RunStartArgs,
|
||||||
|
CodexSandbox,
|
||||||
|
CodexApproval,
|
||||||
} from "../../lib/api";
|
} from "../../lib/api";
|
||||||
import type { Session, Lane } from "../../lib/types";
|
import type { Session, Lane, LaneTool } from "../../lib/types";
|
||||||
import { TerminalView } from "./TerminalView";
|
import { TerminalView } from "./TerminalView";
|
||||||
import { RunSetup } from "./RunSetup";
|
import { RunSetup } from "./RunSetup";
|
||||||
import { ActiveRunsSwitcher } from "./RunHistory";
|
import { ActiveRunsSwitcher } from "./RunHistory";
|
||||||
@@ -40,7 +42,7 @@ export interface LaneConsolePaneProps {
|
|||||||
showLaneSelector: boolean;
|
showLaneSelector: boolean;
|
||||||
onLaneIdChange: (id: number) => void;
|
onLaneIdChange: (id: number) => void;
|
||||||
onLaneCreated: (lane: Lane) => void;
|
onLaneCreated: (lane: Lane) => void;
|
||||||
binaryStatus: { found: boolean; path: string | null } | null;
|
binaryStatus: Record<LaneTool, { found: boolean; path: string | null }> | null;
|
||||||
cwdSuggestions: CwdSuggestion[];
|
cwdSuggestions: CwdSuggestion[];
|
||||||
activeRuns: RunListResponse | null;
|
activeRuns: RunListResponse | null;
|
||||||
/** Active Claude Code sessions started outside the dashboard, listed in the
|
/** Active Claude Code sessions started outside the dashboard, listed in the
|
||||||
@@ -79,8 +81,11 @@ export function LaneConsolePane({
|
|||||||
const [busy, setBusy] = useState<"start" | "kill" | "attach" | null>(null);
|
const [busy, setBusy] = useState<"start" | "kill" | "attach" | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [runHistory, setRunHistory] = useState<DashboardRunHistoryItem[]>([]);
|
const [runHistory, setRunHistory] = useState<DashboardRunHistoryItem[]>([]);
|
||||||
|
const [sandbox, setSandbox] = useState<CodexSandbox>("workspace-write");
|
||||||
|
const [approval, setApproval] = useState<CodexApproval>("on-request");
|
||||||
|
|
||||||
const currentLane = laneId !== null ? lanes.find((l) => l.id === laneId) : null;
|
const currentLane = laneId !== null ? lanes.find((l) => l.id === laneId) : null;
|
||||||
|
const laneTool = currentLane?.tool ?? "claude";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onHasActiveRunChange?.(handle !== null);
|
onHasActiveRunChange?.(handle !== null);
|
||||||
@@ -239,6 +244,9 @@ export function LaneConsolePane({
|
|||||||
permissionMode: args.permissionMode,
|
permissionMode: args.permissionMode,
|
||||||
resumeSessionId: args.resumeSessionId,
|
resumeSessionId: args.resumeSessionId,
|
||||||
effort: args.effort || undefined,
|
effort: args.effort || undefined,
|
||||||
|
sandbox: args.sandbox,
|
||||||
|
approval: args.approval,
|
||||||
|
resumeLast: args.resumeLast,
|
||||||
});
|
});
|
||||||
} catch (laneErr: unknown) {
|
} catch (laneErr: unknown) {
|
||||||
const msg = laneErr instanceof Error ? laneErr.message : String(laneErr);
|
const msg = laneErr instanceof Error ? laneErr.message : String(laneErr);
|
||||||
@@ -426,7 +434,7 @@ export function LaneConsolePane({
|
|||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{binaryStatus && !binaryStatus.found && (
|
{binaryStatus && !binaryStatus[laneTool]?.found && (
|
||||||
<div className="rounded-lg border border-status-danger/40 bg-status-danger/10 px-4 py-3 text-sm text-status-danger flex items-center gap-2">
|
<div className="rounded-lg border border-status-danger/40 bg-status-danger/10 px-4 py-3 text-sm text-status-danger flex items-center gap-2">
|
||||||
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
||||||
<span>{t("binary.missing")}</span>
|
<span>{t("binary.missing")}</span>
|
||||||
@@ -454,7 +462,7 @@ export function LaneConsolePane({
|
|||||||
onPermissionModeChange={setPermissionMode}
|
onPermissionModeChange={setPermissionMode}
|
||||||
effort={effort}
|
effort={effort}
|
||||||
onEffortChange={setEffort}
|
onEffortChange={setEffort}
|
||||||
binaryFound={binaryStatus?.found ?? true}
|
binaryFound={binaryStatus?.[laneTool]?.found ?? true}
|
||||||
busy={busy === "start"}
|
busy={busy === "start"}
|
||||||
onStart={onStartFromSetup}
|
onStart={onStartFromSetup}
|
||||||
activeRuns={activeRuns}
|
activeRuns={activeRuns}
|
||||||
@@ -463,6 +471,12 @@ export function LaneConsolePane({
|
|||||||
onResumeSessionChange={setResumeSession}
|
onResumeSessionChange={setResumeSession}
|
||||||
runHistory={runHistory}
|
runHistory={runHistory}
|
||||||
onResumeFromHistory={onResumeFromHistory}
|
onResumeFromHistory={onResumeFromHistory}
|
||||||
|
tool={laneTool}
|
||||||
|
sandbox={sandbox}
|
||||||
|
onSandboxChange={setSandbox}
|
||||||
|
approval={approval}
|
||||||
|
onApprovalChange={setApproval}
|
||||||
|
onResumeLast={() => onStartFromSetup({ laneId: laneId ?? 0, cwd, resumeLast: true })}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 min-h-0 flex flex-col">
|
<div className="flex-1 min-h-0 flex flex-col">
|
||||||
|
|||||||
@@ -47,8 +47,10 @@ import type {
|
|||||||
EffortLevel,
|
EffortLevel,
|
||||||
PermissionMode,
|
PermissionMode,
|
||||||
RunStartArgs,
|
RunStartArgs,
|
||||||
|
CodexSandbox,
|
||||||
|
CodexApproval,
|
||||||
} from "../../lib/api";
|
} from "../../lib/api";
|
||||||
import type { Session } from "../../lib/types";
|
import type { Session, LaneTool } from "../../lib/types";
|
||||||
import { Select } from "../Select";
|
import { Select } from "../Select";
|
||||||
|
|
||||||
// Minimal SlashCommand type (from deleted RunConsole)
|
// Minimal SlashCommand type (from deleted RunConsole)
|
||||||
@@ -117,6 +119,15 @@ interface RunSetupProps {
|
|||||||
slashCommands?: SlashCommand[];
|
slashCommands?: SlashCommand[];
|
||||||
runHistory: DashboardRunHistoryItem[];
|
runHistory: DashboardRunHistoryItem[];
|
||||||
onResumeFromHistory: (item: DashboardRunHistoryItem) => void;
|
onResumeFromHistory: (item: DashboardRunHistoryItem) => void;
|
||||||
|
/** The selected lane's CLI. Decides which advanced fields render. */
|
||||||
|
tool: LaneTool;
|
||||||
|
sandbox: CodexSandbox;
|
||||||
|
onSandboxChange: (s: CodexSandbox) => void;
|
||||||
|
approval: CodexApproval;
|
||||||
|
onApprovalChange: (a: CodexApproval) => void;
|
||||||
|
/** Codex lanes resume with `--last`: the sessions table holds Claude
|
||||||
|
* sessions only, so there is nothing for the picker to list. */
|
||||||
|
onResumeLast: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RunSetup(props: RunSetupProps) {
|
export function RunSetup(props: RunSetupProps) {
|
||||||
@@ -154,19 +165,29 @@ export function RunSetup(props: RunSetupProps) {
|
|||||||
</div>
|
</div>
|
||||||
{resumePicked && (
|
{resumePicked && (
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<SessionPicker
|
{props.tool === "claude" ? (
|
||||||
selected={props.resumeSession}
|
<SessionPicker
|
||||||
onSelect={(s) => {
|
selected={props.resumeSession}
|
||||||
props.onResumeSessionChange(s);
|
onSelect={(s) => {
|
||||||
// Pass the picked session explicitly: the parent's state
|
props.onResumeSessionChange(s);
|
||||||
// update has not landed yet on this tick, so reading
|
// Pass the picked session explicitly: the parent's state
|
||||||
// props.resumeSession here would resume nothing.
|
// update has not landed yet on this tick, so reading
|
||||||
if (s && !props.busy) handleStart(props, s);
|
// props.resumeSession here would resume nothing.
|
||||||
}}
|
if (s && !props.busy) handleStart(props, s);
|
||||||
// Fall back to the typed cwd when no lane is selected — otherwise
|
}}
|
||||||
// an unfiltered picker lists every session from every repo.
|
// Fall back to the typed cwd when no lane is selected — otherwise
|
||||||
cwd={props.laneCwd || props.cwd.trim() || undefined}
|
// an unfiltered picker lists every session from every repo.
|
||||||
/>
|
cwd={props.laneCwd || props.cwd.trim() || undefined}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => props.onResumeLast()}
|
||||||
|
disabled={props.busy}
|
||||||
|
className="px-3 py-1.5 text-[11px] rounded-md bg-surface-2 border border-border hover:border-accent/50 text-fg-primary hover:text-accent transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{t("resume.lastOption")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -211,18 +232,44 @@ export function RunSetup(props: RunSetupProps) {
|
|||||||
<Field label={t("fields.model")}>
|
<Field label={t("fields.model")}>
|
||||||
<ModelPicker value={props.model} onChange={props.onModelChange} />
|
<ModelPicker value={props.model} onChange={props.onModelChange} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label={t("fields.permissionMode")}>
|
{props.tool === "codex" ? (
|
||||||
<Select<PermissionMode>
|
<>
|
||||||
value={props.permissionMode}
|
<Field label={t("fields.sandbox")}>
|
||||||
onChange={props.onPermissionModeChange}
|
<Select<CodexSandbox>
|
||||||
options={[
|
value={props.sandbox}
|
||||||
{ value: "acceptEdits", label: t("fields.permissionAcceptEdits") },
|
onChange={props.onSandboxChange}
|
||||||
{ value: "default", label: t("fields.permissionDefault") },
|
options={[
|
||||||
{ value: "plan", label: t("fields.permissionPlan") },
|
{ value: "read-only", label: t("fields.sandboxReadOnly") },
|
||||||
{ value: "bypassPermissions", label: t("fields.permissionBypass") },
|
{ value: "workspace-write", label: t("fields.sandboxWorkspaceWrite") },
|
||||||
]}
|
{ value: "danger-full-access", label: t("fields.sandboxFullAccess") },
|
||||||
/>
|
]}
|
||||||
</Field>
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label={t("fields.approval")}>
|
||||||
|
<Select<CodexApproval>
|
||||||
|
value={props.approval}
|
||||||
|
onChange={props.onApprovalChange}
|
||||||
|
options={[
|
||||||
|
{ value: "on-request", label: t("fields.approvalOnRequest") },
|
||||||
|
{ value: "never", label: t("fields.approvalNever") },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Field label={t("fields.permissionMode")}>
|
||||||
|
<Select<PermissionMode>
|
||||||
|
value={props.permissionMode}
|
||||||
|
onChange={props.onPermissionModeChange}
|
||||||
|
options={[
|
||||||
|
{ value: "acceptEdits", label: t("fields.permissionAcceptEdits") },
|
||||||
|
{ value: "default", label: t("fields.permissionDefault") },
|
||||||
|
{ value: "plan", label: t("fields.permissionPlan") },
|
||||||
|
{ value: "bypassPermissions", label: t("fields.permissionBypass") },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
<Field label={t("fields.effort")}>
|
<Field label={t("fields.effort")}>
|
||||||
<Select<EffortLevel>
|
<Select<EffortLevel>
|
||||||
value={props.effort}
|
value={props.effort}
|
||||||
@@ -236,7 +283,14 @@ export function RunSetup(props: RunSetupProps) {
|
|||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{props.permissionMode === "bypassPermissions" && (
|
{props.tool === "codex" && props.sandbox === "danger-full-access" && (
|
||||||
|
<div className="mx-4 mb-3 rounded-md border border-status-danger/40 bg-status-danger/10 px-3 py-2 text-[11px] text-status-danger flex items-start gap-2">
|
||||||
|
<ShieldAlert className="w-3.5 h-3.5 flex-shrink-0 mt-0.5" />
|
||||||
|
<span>{t("hint.sandboxWarning")}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{props.tool === "claude" && props.permissionMode === "bypassPermissions" && (
|
||||||
<div className="mx-4 mb-3 rounded-md border border-status-danger/40 bg-status-danger/10 px-3 py-2 text-[11px] text-status-danger flex items-start gap-2">
|
<div className="mx-4 mb-3 rounded-md border border-status-danger/40 bg-status-danger/10 px-3 py-2 text-[11px] text-status-danger flex items-start gap-2">
|
||||||
<ShieldAlert className="w-3.5 h-3.5 flex-shrink-0 mt-0.5" />
|
<ShieldAlert className="w-3.5 h-3.5 flex-shrink-0 mt-0.5" />
|
||||||
<span>{t("hint.permissionWarning")}</span>
|
<span>{t("hint.permissionWarning")}</span>
|
||||||
@@ -285,15 +339,19 @@ export function RunSetup(props: RunSetupProps) {
|
|||||||
* out of the picker, before the parent's state has caught up. */
|
* out of the picker, before the parent's state has caught up. */
|
||||||
function handleStart(props: RunSetupProps, session?: Session) {
|
function handleStart(props: RunSetupProps, session?: Session) {
|
||||||
const resume = session ?? props.resumeSession;
|
const resume = session ?? props.resumeSession;
|
||||||
|
const codex = props.tool === "codex";
|
||||||
props.onStart({
|
props.onStart({
|
||||||
laneId: props.laneId,
|
laneId: props.laneId,
|
||||||
// A resume is pinned to its own session's folder — that's what the locked
|
// A resume is pinned to its own session's folder — that's what the locked
|
||||||
// cwd field shows, so it's what gets sent.
|
// cwd field shows, so it's what gets sent.
|
||||||
cwd: resume?.cwd || props.cwd || undefined,
|
cwd: resume?.cwd || props.cwd || undefined,
|
||||||
model: props.model || undefined,
|
model: props.model || undefined,
|
||||||
permissionMode: props.permissionMode || undefined,
|
permissionMode: codex ? undefined : props.permissionMode || undefined,
|
||||||
|
sandbox: codex ? props.sandbox : undefined,
|
||||||
|
approval: codex ? props.approval : undefined,
|
||||||
effort: props.effort || undefined,
|
effort: props.effort || undefined,
|
||||||
resumeSessionId: resume?.id || undefined,
|
resumeSessionId: codex ? undefined : resume?.id || undefined,
|
||||||
|
resumeLast: codex && !resume ? true : undefined,
|
||||||
initialPrompt: props.prompt || undefined,
|
initialPrompt: props.prompt || undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,10 @@ function baseProps() {
|
|||||||
showLaneSelector: false,
|
showLaneSelector: false,
|
||||||
onLaneIdChange: vi.fn(),
|
onLaneIdChange: vi.fn(),
|
||||||
onLaneCreated: vi.fn(),
|
onLaneCreated: vi.fn(),
|
||||||
binaryStatus: { found: true, path: "/usr/local/bin/claude" },
|
binaryStatus: {
|
||||||
|
claude: { found: true, path: "/usr/local/bin/claude" },
|
||||||
|
codex: { found: true, path: "/usr/local/bin/codex" },
|
||||||
|
},
|
||||||
cwdSuggestions: [],
|
cwdSuggestions: [],
|
||||||
activeRuns: { items: [] },
|
activeRuns: { items: [] },
|
||||||
wsConnected: true,
|
wsConnected: true,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
import { MemoryRouter } from "react-router-dom";
|
import { MemoryRouter } from "react-router-dom";
|
||||||
import { fireEvent, render, screen } from "@testing-library/react";
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import { RunSetup } from "../RunSetup";
|
import { RunSetup } from "../RunSetup";
|
||||||
@@ -47,6 +48,9 @@ function renderSetup(overrides: Partial<React.ComponentProps<typeof RunSetup>> =
|
|||||||
onStart: vi.fn(),
|
onStart: vi.fn(),
|
||||||
onResumeSessionChange: vi.fn(),
|
onResumeSessionChange: vi.fn(),
|
||||||
onResumeFromHistory: vi.fn(),
|
onResumeFromHistory: vi.fn(),
|
||||||
|
onSandboxChange: vi.fn(),
|
||||||
|
onApprovalChange: vi.fn(),
|
||||||
|
onResumeLast: vi.fn(),
|
||||||
};
|
};
|
||||||
const utils = render(
|
const utils = render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
@@ -64,6 +68,9 @@ function renderSetup(overrides: Partial<React.ComponentProps<typeof RunSetup>> =
|
|||||||
resumeSession={null}
|
resumeSession={null}
|
||||||
slashCommands={[]}
|
slashCommands={[]}
|
||||||
runHistory={[]}
|
runHistory={[]}
|
||||||
|
tool="claude"
|
||||||
|
sandbox="workspace-write"
|
||||||
|
approval="on-request"
|
||||||
{...spies}
|
{...spies}
|
||||||
{...overrides}
|
{...overrides}
|
||||||
/>
|
/>
|
||||||
@@ -273,6 +280,9 @@ describe("RunSetup — resume picker scopes sessions to the selected lane", () =
|
|||||||
slashCommands={[]}
|
slashCommands={[]}
|
||||||
runHistory={[]}
|
runHistory={[]}
|
||||||
laneCwd="/Users/tester/lane-b"
|
laneCwd="/Users/tester/lane-b"
|
||||||
|
tool="claude"
|
||||||
|
sandbox="workspace-write"
|
||||||
|
approval="on-request"
|
||||||
onPromptChange={vi.fn()}
|
onPromptChange={vi.fn()}
|
||||||
onCwdChange={vi.fn()}
|
onCwdChange={vi.fn()}
|
||||||
onModelChange={vi.fn()}
|
onModelChange={vi.fn()}
|
||||||
@@ -281,6 +291,9 @@ describe("RunSetup — resume picker scopes sessions to the selected lane", () =
|
|||||||
onStart={vi.fn()}
|
onStart={vi.fn()}
|
||||||
onResumeSessionChange={vi.fn()}
|
onResumeSessionChange={vi.fn()}
|
||||||
onResumeFromHistory={vi.fn()}
|
onResumeFromHistory={vi.fn()}
|
||||||
|
onSandboxChange={vi.fn()}
|
||||||
|
onApprovalChange={vi.fn()}
|
||||||
|
onResumeLast={vi.fn()}
|
||||||
/>
|
/>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@@ -291,3 +304,29 @@ describe("RunSetup — resume picker scopes sessions to the selected lane", () =
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("RunSetup — per-tool fields", () => {
|
||||||
|
it("shows sandbox and approval instead of permission mode for a codex lane", () => {
|
||||||
|
renderSetup({ tool: "codex" });
|
||||||
|
expect(screen.getByText(/sandbox/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/approval/i)).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText(/permission mode/i)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps permission mode for a claude lane", () => {
|
||||||
|
renderSetup({ tool: "claude" });
|
||||||
|
expect(screen.getByText(/permission mode/i)).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText(/sandbox/i)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends codex fields on start", async () => {
|
||||||
|
const onStart = vi.fn();
|
||||||
|
const user = userEvent.setup();
|
||||||
|
renderSetup({ tool: "codex", onStart, prompt: "go" });
|
||||||
|
await user.click(screen.getByText(i18n.t("run:actions.start")));
|
||||||
|
expect(onStart).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ sandbox: "workspace-write", approval: "on-request" })
|
||||||
|
);
|
||||||
|
expect(onStart.mock.calls[0]?.[0]?.permissionMode).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -54,6 +54,9 @@
|
|||||||
"you": "You"
|
"you": "You"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"approval": "Approval",
|
||||||
|
"approvalNever": "Never",
|
||||||
|
"approvalOnRequest": "On request",
|
||||||
"cwd": "Working directory",
|
"cwd": "Working directory",
|
||||||
"cwdGroups": {
|
"cwdGroups": {
|
||||||
"dashboard": "Dashboard server",
|
"dashboard": "Dashboard server",
|
||||||
@@ -75,7 +78,11 @@
|
|||||||
"permissionPlan": "plan (read-only planning)",
|
"permissionPlan": "plan (read-only planning)",
|
||||||
"prompt": "Prompt",
|
"prompt": "Prompt",
|
||||||
"promptPlaceholder": "Ask Claude anything…",
|
"promptPlaceholder": "Ask Claude anything…",
|
||||||
"promptPlaceholderTerminal": "Ask Claude anything…"
|
"promptPlaceholderTerminal": "Ask Claude anything…",
|
||||||
|
"sandbox": "Sandbox",
|
||||||
|
"sandboxFullAccess": "Full access (dangerous)",
|
||||||
|
"sandboxReadOnly": "Read only",
|
||||||
|
"sandboxWorkspaceWrite": "Workspace write"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"cost": "Cost",
|
"cost": "Cost",
|
||||||
@@ -87,6 +94,7 @@
|
|||||||
"hint": {
|
"hint": {
|
||||||
"headlessExplain": "Headless mode is best for scripted tasks where you know exactly what you want. The session can't ask follow-up questions and will hang on permission prompts unless you stay in acceptEdits.",
|
"headlessExplain": "Headless mode is best for scripted tasks where you know exactly what you want. The session can't ask follow-up questions and will hang on permission prompts unless you stay in acceptEdits.",
|
||||||
"permissionWarning": "bypassPermissions allows ALL tool invocations without confirming. Only use for trusted prompts.",
|
"permissionWarning": "bypassPermissions allows ALL tool invocations without confirming. Only use for trusted prompts.",
|
||||||
|
"sandboxWarning": "Codex will run commands with no sandbox.",
|
||||||
"shortcut": "Cmd+Enter / Ctrl+Enter to send"
|
"shortcut": "Cmd+Enter / Ctrl+Enter to send"
|
||||||
},
|
},
|
||||||
"limitations": {
|
"limitations": {
|
||||||
@@ -104,6 +112,7 @@
|
|||||||
"freshHint": "Start a fresh Claude Code session.",
|
"freshHint": "Start a fresh Claude Code session.",
|
||||||
"freshOption": "New session",
|
"freshOption": "New session",
|
||||||
"label": "Source session",
|
"label": "Source session",
|
||||||
|
"lastOption": "Resume last session",
|
||||||
"noSessions": "No sessions found.",
|
"noSessions": "No sessions found.",
|
||||||
"originalCwd": "Original cwd",
|
"originalCwd": "Original cwd",
|
||||||
"pickSession": "Pick a session…",
|
"pickSession": "Pick a session…",
|
||||||
|
|||||||
@@ -53,6 +53,9 @@
|
|||||||
"you": "Bạn"
|
"you": "Bạn"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"approval": "Phê duyệt",
|
||||||
|
"approvalNever": "Không bao giờ",
|
||||||
|
"approvalOnRequest": "Khi cần",
|
||||||
"cwd": "Thư mục làm việc",
|
"cwd": "Thư mục làm việc",
|
||||||
"cwdGroups": {
|
"cwdGroups": {
|
||||||
"dashboard": "Dashboard server",
|
"dashboard": "Dashboard server",
|
||||||
@@ -74,7 +77,11 @@
|
|||||||
"permissionPlan": "plan (chỉ đọc, lập kế hoạch)",
|
"permissionPlan": "plan (chỉ đọc, lập kế hoạch)",
|
||||||
"prompt": "Prompt",
|
"prompt": "Prompt",
|
||||||
"promptPlaceholder": "Hỏi Claude bất cứ điều gì…",
|
"promptPlaceholder": "Hỏi Claude bất cứ điều gì…",
|
||||||
"promptPlaceholderTerminal": "Hỏi Claude bất cứ điều gì…"
|
"promptPlaceholderTerminal": "Hỏi Claude bất cứ điều gì…",
|
||||||
|
"sandbox": "Sandbox",
|
||||||
|
"sandboxFullAccess": "Truy cập đầy đủ (nguy hiểm)",
|
||||||
|
"sandboxReadOnly": "Chỉ đọc",
|
||||||
|
"sandboxWorkspaceWrite": "Ghi vào workspace"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"cost": "Chi phí",
|
"cost": "Chi phí",
|
||||||
@@ -86,6 +93,7 @@
|
|||||||
"hint": {
|
"hint": {
|
||||||
"headlessExplain": "Headless phù hợp cho tác vụ tự động. Phiên không thể hỏi lại, sẽ treo ở prompt xin quyền nếu bạn không giữ acceptEdits.",
|
"headlessExplain": "Headless phù hợp cho tác vụ tự động. Phiên không thể hỏi lại, sẽ treo ở prompt xin quyền nếu bạn không giữ acceptEdits.",
|
||||||
"permissionWarning": "bypassPermissions cho phép gọi mọi tool không cần xác nhận. Chỉ dùng cho prompt bạn tin tưởng.",
|
"permissionWarning": "bypassPermissions cho phép gọi mọi tool không cần xác nhận. Chỉ dùng cho prompt bạn tin tưởng.",
|
||||||
|
"sandboxWarning": "Codex sẽ chạy lệnh mà không có sandbox.",
|
||||||
"shortcut": "Cmd+Enter / Ctrl+Enter để gửi"
|
"shortcut": "Cmd+Enter / Ctrl+Enter để gửi"
|
||||||
},
|
},
|
||||||
"limitations": {
|
"limitations": {
|
||||||
@@ -103,6 +111,7 @@
|
|||||||
"freshHint": "Bắt đầu phiên Claude Code mới.",
|
"freshHint": "Bắt đầu phiên Claude Code mới.",
|
||||||
"freshOption": "Phiên mới",
|
"freshOption": "Phiên mới",
|
||||||
"label": "Phiên nguồn",
|
"label": "Phiên nguồn",
|
||||||
|
"lastOption": "Tiếp tục phiên trước",
|
||||||
"noSessions": "Không tìm thấy phiên.",
|
"noSessions": "Không tìm thấy phiên.",
|
||||||
"originalCwd": "cwd gốc",
|
"originalCwd": "cwd gốc",
|
||||||
"pickSession": "Chọn phiên…",
|
"pickSession": "Chọn phiên…",
|
||||||
|
|||||||
@@ -44,7 +44,15 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../lib/api";
|
||||||
import type { CwdSuggestion, RunListResponse } from "../lib/api";
|
import type { CwdSuggestion, RunListResponse } from "../lib/api";
|
||||||
import type { Lane, LaneFeature, LaneCounts, ProofFeature, Session, WSMessage } from "../lib/types";
|
import type {
|
||||||
|
Lane,
|
||||||
|
LaneFeature,
|
||||||
|
LaneCounts,
|
||||||
|
ProofFeature,
|
||||||
|
Session,
|
||||||
|
WSMessage,
|
||||||
|
LaneTool,
|
||||||
|
} from "../lib/types";
|
||||||
import { eventBus } from "../lib/eventBus";
|
import { eventBus } from "../lib/eventBus";
|
||||||
import { LaneConsolePane } from "../components/run/LaneConsolePane";
|
import { LaneConsolePane } from "../components/run/LaneConsolePane";
|
||||||
import PipelineMap from "../components/lanes/PipelineMap";
|
import PipelineMap from "../components/lanes/PipelineMap";
|
||||||
@@ -109,7 +117,7 @@ function ConsoleArea({
|
|||||||
splitView: SplitViewState;
|
splitView: SplitViewState;
|
||||||
setLayout: (layout: SplitLayout) => void;
|
setLayout: (layout: SplitLayout) => void;
|
||||||
setPaneLaneId: (index: number, id: number) => void;
|
setPaneLaneId: (index: number, id: number) => void;
|
||||||
binaryStatus: { found: boolean; path: string | null } | null;
|
binaryStatus: Record<LaneTool, { found: boolean; path: string | null }> | null;
|
||||||
cwdSuggestions: CwdSuggestion[];
|
cwdSuggestions: CwdSuggestion[];
|
||||||
activeRuns: RunListResponse | null;
|
activeRuns: RunListResponse | null;
|
||||||
externalSessions: Session[];
|
externalSessions: Session[];
|
||||||
@@ -192,9 +200,10 @@ export function Workspace() {
|
|||||||
// started in a terminal tab). They are not dashboard runs, so the run list
|
// started in a terminal tab). They are not dashboard runs, so the run list
|
||||||
// would otherwise show nothing while two agents are working.
|
// would otherwise show nothing while two agents are working.
|
||||||
const [externalSessions, setExternalSessions] = useState<Session[]>([]);
|
const [externalSessions, setExternalSessions] = useState<Session[]>([]);
|
||||||
const [binaryStatus, setBinaryStatus] = useState<{ found: boolean; path: string | null } | null>(
|
const [binaryStatus, setBinaryStatus] = useState<Record<
|
||||||
null
|
LaneTool,
|
||||||
);
|
{ found: boolean; path: string | null }
|
||||||
|
> | null>(null);
|
||||||
const [cwdSuggestions, setCwdSuggestions] = useState<CwdSuggestion[]>([]);
|
const [cwdSuggestions, setCwdSuggestions] = useState<CwdSuggestion[]>([]);
|
||||||
const [defaultCwd, setDefaultCwd] = useState<string>("");
|
const [defaultCwd, setDefaultCwd] = useState<string>("");
|
||||||
const [paneHasActiveRun, setPaneHasActiveRun] = useState(false);
|
const [paneHasActiveRun, setPaneHasActiveRun] = useState(false);
|
||||||
@@ -241,12 +250,11 @@ export function Workspace() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.run
|
Promise.all([api.run.binary("claude"), api.run.binary("codex")])
|
||||||
.binary()
|
.then(([claude, codex]) => setBinaryStatus({ claude, codex }))
|
||||||
.then(setBinaryStatus)
|
|
||||||
// Fetch failure (server unreachable, proxy misrouted, etc.) isn't proof
|
// Fetch failure (server unreachable, proxy misrouted, etc.) isn't proof
|
||||||
// `claude` is missing from PATH — leave the probe unresolved rather than
|
// `claude` or `codex` is missing from PATH — leave the probe unresolved rather than
|
||||||
// showing a misleading "claude missing" banner for an unrelated fault.
|
// showing a misleading "binary missing" banner for an unrelated fault.
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
api.lanes
|
api.lanes
|
||||||
.pipelines()
|
.pipelines()
|
||||||
|
|||||||
Reference in New Issue
Block a user