Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ed5018a2f | |||
| aacd63b85b |
@@ -1,6 +1,6 @@
|
|||||||
# Claude Code Working Guide
|
# Claude Code Working Guide
|
||||||
|
|
||||||
_Last synced: v2.7.8 (2026-10-14) — Delete user, RSA SSH keys, File Explorer, clone + git identity, Artifacts hub._ A change that makes any statement in this file, `AGENTS.md`, or a README wrong — a moved path, a new or changed extension point, a retired rule — updates that file in the same change-set and bumps this marker. **Bump = replace the version/date and a ≤ 1-line summary; never append a changelog here** — details belong in the commit message and `docs/`, because this whole file is loaded into every session.
|
_Last synced: v2.7.9 (2026-10-14) — Add workspace has no AI tool picker (default tool)._ A change that makes any statement in this file, `AGENTS.md`, or a README wrong — a moved path, a new or changed extension point, a retired rule — updates that file in the same change-set and bumps this marker. **Bump = replace the version/date and a ≤ 1-line summary; never append a changelog here** — details belong in the commit message and `docs/`, because this whole file is loaded into every session.
|
||||||
|
|
||||||
## Project mission
|
## Project mission
|
||||||
- Maintain a reliable local-first dashboard for Claude Code session monitoring.
|
- Maintain a reliable local-first dashboard for Claude Code session monitoring.
|
||||||
|
|||||||
@@ -9,8 +9,9 @@
|
|||||||
* manually-typed branch name; "Clone" runs `POST /api/lanes/clone` (an
|
* manually-typed branch name; "Clone" runs `POST /api/lanes/clone` (an
|
||||||
* https/ssh URL cloned into a folder of the user's, with their OWN SSH key or
|
* https/ssh URL cloned into a folder of the user's, with their OWN SSH key or
|
||||||
* HTTPS login — a 409 `ECREDENTIALS` opens a login row, `ESSHAUTH` points to
|
* HTTPS login — a 409 `ECREDENTIALS` opens a login row, `ESSHAUTH` points to
|
||||||
* Settings → Git). The user also picks which CLI drives the lane
|
* Settings → Git). There is no AI tool picker: the lane is created with the
|
||||||
* (Claude Code or Codex) — this choice is immutable after creation. Either
|
* default AI tool (Settings → AI Tools), and each new session picks its own
|
||||||
|
* AI on the workspace home. Either
|
||||||
* mode's path field can be filled by typing, by the CwdAutocomplete
|
* mode's path field can be filled by typing, by the CwdAutocomplete
|
||||||
* suggestions, or by Browse (`PathPickButton`: the OS's own folder picker
|
* suggestions, or by Browse (`PathPickButton`: the OS's own folder picker
|
||||||
* opened by the server, the in-app `FolderBrowseModal` where it cannot be) —
|
* opened by the server, the in-app `FolderBrowseModal` where it cannot be) —
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useId, useRef, useState } from "react";
|
import { useCallback, useEffect, useId, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ChevronDown, X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { useDialog } from "../../hooks/useDialog";
|
import { useDialog } from "../../hooks/useDialog";
|
||||||
import { CwdAutocomplete } from "../run/RunSetup";
|
import { CwdAutocomplete } from "../run/RunSetup";
|
||||||
import { PathPickButton } from "../PathPickButton";
|
import { PathPickButton } from "../PathPickButton";
|
||||||
@@ -32,7 +33,6 @@ import { api } from "../../lib/api";
|
|||||||
import type { CwdSuggestion } from "../../lib/api";
|
import type { CwdSuggestion } from "../../lib/api";
|
||||||
import type { Lane, LaneWorktree } from "../../lib/types";
|
import type { Lane, LaneWorktree } from "../../lib/types";
|
||||||
import { useAiTools } from "../../hooks/useAiTools";
|
import { useAiTools } from "../../hooks/useAiTools";
|
||||||
import { AiToolIcon } from "../AiToolIcon";
|
|
||||||
import { readDefaultTool } from "../../lib/sessionDefaults";
|
import { readDefaultTool } from "../../lib/sessionDefaults";
|
||||||
|
|
||||||
/** Polls the lane until the background `git worktree add` finishes (status
|
/** Polls the lane until the background `git worktree add` finishes (status
|
||||||
@@ -135,27 +135,18 @@ export function AddLaneModal({
|
|||||||
const [branchMode, setBranchMode] = useState<"new" | "existing">("new");
|
const [branchMode, setBranchMode] = useState<"new" | "existing">("new");
|
||||||
const [base, setBase] = useState("");
|
const [base, setBase] = useState("");
|
||||||
const [branchesError, setBranchesError] = useState<string | null>(null);
|
const [branchesError, setBranchesError] = useState<string | null>(null);
|
||||||
// null = the default AI tool (Settings → AI Tools), resolved at submit time.
|
// No picker: a workspace's tool is only its DEFAULT — each new session
|
||||||
const [pickedTool, setPickedTool] = useState<string | null>(null);
|
// picks its own AI on the workspace home. This is the default AI tool
|
||||||
|
// (Settings → AI Tools), the account's own pick first while it is
|
||||||
|
// installed and on.
|
||||||
const aiTools = useAiTools();
|
const aiTools = useAiTools();
|
||||||
// The account's own pick (first-run setup), while it is installed and on.
|
|
||||||
const ownTool = readDefaultTool();
|
const ownTool = readDefaultTool();
|
||||||
const defaultTool =
|
const tool =
|
||||||
(ownTool && aiTools.tools.some((x) => x.id === ownTool && x.installed && x.enabled)
|
(ownTool && aiTools.tools.some((x) => x.id === ownTool && x.installed && x.enabled)
|
||||||
? ownTool
|
? ownTool
|
||||||
: null) ??
|
: null) ??
|
||||||
aiTools.defaultId ??
|
aiTools.defaultId ??
|
||||||
"claude";
|
"claude";
|
||||||
const tool = pickedTool ?? defaultTool;
|
|
||||||
// Installed + enabled, the default first.
|
|
||||||
const toolChoices = aiTools.tools
|
|
||||||
.filter((x) => x.installed && x.enabled)
|
|
||||||
.sort((a, b) => Number(b.id === defaultTool) - Number(a.id === defaultTool));
|
|
||||||
const [toolListOpen, setToolListOpen] = useState(false);
|
|
||||||
const pickTool = (id: string) => {
|
|
||||||
setPickedTool(id === defaultTool ? null : id);
|
|
||||||
setToolListOpen(false);
|
|
||||||
};
|
|
||||||
const noTools = aiTools.loaded && aiTools.defaultId === null;
|
const noTools = aiTools.loaded && aiTools.defaultId === null;
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -177,7 +168,6 @@ export function AddLaneModal({
|
|||||||
setWorktrees([]);
|
setWorktrees([]);
|
||||||
setBranchMode("new");
|
setBranchMode("new");
|
||||||
setBase("");
|
setBase("");
|
||||||
setPickedTool(null);
|
|
||||||
setBranchesError(null);
|
setBranchesError(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
@@ -501,64 +491,14 @@ export function AddLaneModal({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
{noTools && (
|
||||||
{noTools ? (
|
<p className="rounded-md border border-status-warning/40 bg-status-warning/10 px-2 py-1.5 text-xs text-status-warning">
|
||||||
<p className="rounded-md border border-status-warning/40 bg-status-warning/10 px-2 py-1.5 text-xs text-status-warning">
|
{t("addLaneNoTools")}{" "}
|
||||||
{t("addLaneNoTools")}{" "}
|
<a href="/settings/agents" className="underline">
|
||||||
<a href="/settings/agents" className="underline">
|
{t("addLaneNoToolsLink")}
|
||||||
{t("addLaneNoToolsLink")}
|
</a>
|
||||||
</a>
|
</p>
|
||||||
</p>
|
)}
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
aria-expanded={toolListOpen}
|
|
||||||
disabled={toolChoices.length < 2}
|
|
||||||
onClick={() => setToolListOpen((o) => !o)}
|
|
||||||
className="flex w-full items-center gap-2 rounded-md border border-border bg-surface-2 px-2.5 py-1.5 text-left text-sm text-fg-primary hover:bg-surface-3 disabled:cursor-default disabled:hover:bg-surface-2"
|
|
||||||
>
|
|
||||||
<AiToolIcon tool={{ id: tool, label: aiTools.label(tool) }} className="h-5 w-5" />
|
|
||||||
<span className="min-w-0 flex-1 truncate">{aiTools.label(tool)}</span>
|
|
||||||
{tool === defaultTool && (
|
|
||||||
<span className="rounded-full bg-accent/15 px-2 py-0.5 text-[10px] font-medium text-accent">
|
|
||||||
{t("addLaneToolDefaultBadge")}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{toolChoices.length > 1 && (
|
|
||||||
<ChevronDown
|
|
||||||
aria-hidden
|
|
||||||
className={`h-4 w-4 text-fg-muted transition-transform ${toolListOpen ? "rotate-180" : ""}`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
{toolListOpen && (
|
|
||||||
<div
|
|
||||||
role="group"
|
|
||||||
aria-label={t("addLaneToolHint")}
|
|
||||||
className="mt-1.5 grid grid-cols-2 gap-1.5 sm:grid-cols-3"
|
|
||||||
>
|
|
||||||
{toolChoices.map((x) => (
|
|
||||||
<button
|
|
||||||
key={x.id}
|
|
||||||
type="button"
|
|
||||||
aria-pressed={x.id === tool}
|
|
||||||
onClick={() => pickTool(x.id)}
|
|
||||||
className={`flex items-center gap-2 rounded-md border px-2 py-1.5 text-left text-xs transition-colors ${
|
|
||||||
x.id === tool
|
|
||||||
? "border-accent bg-accent/10 text-accent"
|
|
||||||
: "border-border bg-surface-2 text-fg-secondary hover:bg-surface-3 hover:text-fg-primary"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<AiToolIcon tool={x} className="h-5 w-5" />
|
|
||||||
<span className="min-w-0 flex-1 truncate">{x.label}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs text-fg-secondary" htmlFor="add-lane-repo">
|
<label className="mb-1 block text-xs text-fg-secondary" htmlFor="add-lane-repo">
|
||||||
|
|||||||
@@ -670,46 +670,18 @@ describe("AddLaneModal — create/update readme", () => {
|
|||||||
describe("AddLaneModal — no proxy here", () => {
|
describe("AddLaneModal — no proxy here", () => {
|
||||||
it("asks for no AI Proxy: a new session picks its own", async () => {
|
it("asks for no AI Proxy: a new session picks its own", async () => {
|
||||||
renderModal({});
|
renderModal({});
|
||||||
await screen.findByRole("button", { name: /Claude Code.*Default/ });
|
await waitFor(() => expect(api.aiTools.list).toHaveBeenCalled());
|
||||||
expect(screen.queryByRole("radio", { name: "AI Proxy" })).toBeNull();
|
expect(screen.queryByRole("radio", { name: "AI Proxy" })).toBeNull();
|
||||||
expect(api.aiProxies.list).not.toHaveBeenCalled();
|
expect(api.aiProxies.list).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("AddLaneModal — tool selection", () => {
|
describe("AddLaneModal — tool selection", () => {
|
||||||
it("sends the picked tool when adopting a repo", async () => {
|
it("offers no tool picker: a new session picks its own AI", async () => {
|
||||||
vi.mocked(api.lanes.ensure).mockResolvedValue({
|
renderModal({});
|
||||||
lane: laneFixture({ id: 23, kind: "adopted", status: "idle" }),
|
await waitFor(() => expect(api.aiTools.list).toHaveBeenCalled());
|
||||||
created: true,
|
expect(screen.queryByRole("button", { name: /Claude Code/ })).toBeNull();
|
||||||
});
|
expect(screen.queryByRole("button", { name: "Codex" })).toBeNull();
|
||||||
const onAdded = vi.fn();
|
|
||||||
const onClose = vi.fn();
|
|
||||||
renderModal({ onAdded, onClose });
|
|
||||||
const user = userEvent.setup();
|
|
||||||
|
|
||||||
await user.click(screen.getByRole("button", { name: "Repo" }));
|
|
||||||
const dirField = screen.getByLabelText("Directory");
|
|
||||||
await focusField(user, dirField);
|
|
||||||
await user.type(dirField, "/tmp/demo");
|
|
||||||
// One button showing the default; it opens every installed + enabled tool.
|
|
||||||
const picker = await screen.findByRole("button", { name: /Claude Code.*Default/ });
|
|
||||||
expect(picker).toHaveAttribute("aria-expanded", "false");
|
|
||||||
await user.click(picker);
|
|
||||||
const list = screen.getByRole("group", { name: "Default AI tool" });
|
|
||||||
expect(
|
|
||||||
within(list)
|
|
||||||
.getAllByRole("button")
|
|
||||||
.map((b) => b.textContent)
|
|
||||||
).toEqual(["Claude Code", "Codex", "Gemini"]);
|
|
||||||
await user.click(within(list).getByRole("button", { name: "Codex" }));
|
|
||||||
// The picked tool replaces the default on the button, and the list closes.
|
|
||||||
expect(screen.getByRole("button", { name: "Codex", expanded: false })).toBeInTheDocument();
|
|
||||||
expect(screen.queryByRole("button", { name: "Gemini" })).toBeNull();
|
|
||||||
await user.click(screen.getByRole("button", { name: /add workspace|create/i }));
|
|
||||||
|
|
||||||
await waitFor(() =>
|
|
||||||
expect(api.lanes.ensure).toHaveBeenCalledWith(expect.objectContaining({ tool: "codex" }))
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("says so, with a link to Settings, when no tool is installed and enabled", async () => {
|
it("says so, with a link to Settings, when no tool is installed and enabled", async () => {
|
||||||
|
|||||||
@@ -31,10 +31,8 @@
|
|||||||
"addLaneSetupTitle": "Setup",
|
"addLaneSetupTitle": "Setup",
|
||||||
"addLaneTitleLabel": "Title",
|
"addLaneTitleLabel": "Title",
|
||||||
"addLaneTitlePlaceholder": "Optional",
|
"addLaneTitlePlaceholder": "Optional",
|
||||||
"addLaneToolDefaultBadge": "Default",
|
|
||||||
"addLaneNoTools": "No AI tool is installed and enabled.",
|
"addLaneNoTools": "No AI tool is installed and enabled.",
|
||||||
"addLaneNoToolsLink": "Open Settings → AI Tools",
|
"addLaneNoToolsLink": "Open Settings → AI Tools",
|
||||||
"addLaneToolHint": "Default AI tool",
|
|
||||||
"addLaneWorktreeDetached": "(detached)",
|
"addLaneWorktreeDetached": "(detached)",
|
||||||
"addLaneWorktreesMore": "+{{count}} more",
|
"addLaneWorktreesMore": "+{{count}} more",
|
||||||
"addLaneWorktreesTitle": "Worktrees of this repo ({{count}})",
|
"addLaneWorktreesTitle": "Worktrees of this repo ({{count}})",
|
||||||
|
|||||||
@@ -31,10 +31,8 @@
|
|||||||
"addLaneSetupTitle": "Thiết lập",
|
"addLaneSetupTitle": "Thiết lập",
|
||||||
"addLaneTitleLabel": "Tiêu đề",
|
"addLaneTitleLabel": "Tiêu đề",
|
||||||
"addLaneTitlePlaceholder": "Không bắt buộc",
|
"addLaneTitlePlaceholder": "Không bắt buộc",
|
||||||
"addLaneToolDefaultBadge": "Mặc định",
|
|
||||||
"addLaneNoTools": "Chưa có AI tool nào được cài và bật.",
|
"addLaneNoTools": "Chưa có AI tool nào được cài và bật.",
|
||||||
"addLaneNoToolsLink": "Mở Cài đặt → AI Tools",
|
"addLaneNoToolsLink": "Mở Cài đặt → AI Tools",
|
||||||
"addLaneToolHint": "AI tool mặc định",
|
|
||||||
"addLaneWorktreeDetached": "(detached)",
|
"addLaneWorktreeDetached": "(detached)",
|
||||||
"addLaneWorktreesMore": "+{{count}} nữa",
|
"addLaneWorktreesMore": "+{{count}} nữa",
|
||||||
"addLaneWorktreesTitle": "Worktree của repo này ({{count}})",
|
"addLaneWorktreesTitle": "Worktree của repo này ({{count}})",
|
||||||
|
|||||||
@@ -7178,7 +7178,7 @@ exports[`screen snapshots > Settings — about 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="text-status-warning/90"
|
class="text-status-warning/90"
|
||||||
>
|
>
|
||||||
UI build v2.7.8 (rebuild client to match)
|
UI build v2.7.9 (rebuild client to match)
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -94,7 +94,7 @@ Both modes' path field has a **Browse** button next to it that opens the operati
|
|||||||
|
|
||||||
## Workspace tool: Claude or Codex
|
## Workspace tool: Claude or Codex
|
||||||
|
|
||||||
A workspace has a default tool: **Claude** (default) or **Codex** (or any AI Tools id), chosen in the "+ Add workspace" modal and immutable once the workspace exists. Each NEW session picks its own in the workspace home's prompt box — the **AI** select at the left of the box (beside it, on the right, ONE model chip holding the model list and Effort; AI Proxy, Permission and Local / New worktree sit in a row under the box, like Claude Desktop), lists every installed, enabled tool and starts on the workspace's (its last row, **Open terminal**, is no tool: it opens a plain shell tab right away, `shell: true` in the first free slot, and leaves the picked AI alone) — and `POST /api/lanes/:id/start` takes it as `tool`; a resume keeps the tool its session ran on (`sessions.tool`). The sidebar draws each session's tool icon at the right of its row.
|
A workspace has a default tool: **Claude** (default) or **Codex** (or any AI Tools id), set to the default AI tool (Settings → AI Tools) when the "+ Add workspace" modal creates it — the modal has no tool picker — and immutable once the workspace exists. Each NEW session picks its own in the workspace home's prompt box — the **AI** select at the left of the box (beside it, on the right, ONE model chip holding the model list and Effort; AI Proxy, Permission and Local / New worktree sit in a row under the box, like Claude Desktop), lists every installed, enabled tool and starts on the workspace's (its last row, **Open terminal**, is no tool: it opens a plain shell tab right away, `shell: true` in the first free slot, and leaves the picked AI alone) — and `POST /api/lanes/:id/start` takes it as `tool`; a resume keeps the tool its session ran on (`sessions.tool`). The sidebar draws each session's tool icon at the right of its row.
|
||||||
|
|
||||||
- **`claude` (default)** — `claude` binary is invoked; the session's hooks, messages, and tool calls flow through the dashboard. Claude workspaces render Sessions, Analytics, cost, and stage-detection badges; liveness is tracked by the Claude Code session.
|
- **`claude` (default)** — `claude` binary is invoked; the session's hooks, messages, and tool calls flow through the dashboard. Claude workspaces render Sessions, Analytics, cost, and stage-detection badges; liveness is tracked by the Claude Code session.
|
||||||
- **`codex` (experimental)** — `codex` binary is invoked instead. The home's prompt box shows Sandbox and Approval in place of Claude's permission mode. Codex workspaces carry **no pipeline**: no stage chip, no time-on-stage, no pipeline or feature picker and no PipelineMap, and nothing infers or advances their stage. They also get no Sessions/Analytics/cost. (The console pre-fills nothing into the prompt box, for either tool.)
|
- **`codex` (experimental)** — `codex` binary is invoked instead. The home's prompt box shows Sandbox and Approval in place of Claude's permission mode. Codex workspaces carry **no pipeline**: no stage chip, no time-on-stage, no pipeline or feature picker and no PipelineMap, and nothing infers or advances their stage. They also get no Sessions/Analytics/cost. (The console pre-fills nothing into the prompt box, for either tool.)
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "2.7.8",
|
"version": "2.7.9",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "2.7.8",
|
"version": "2.7.9",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "2.7.8",
|
"version": "2.7.9",
|
||||||
"bin": {
|
"bin": {
|
||||||
"agenthub": "bin/agenthub.js"
|
"agenthub": "bin/agenthub.js"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user