feat(workspace): AI tool per session, one place to start a session, ended sessions open like Claude Desktop
- A session picks its own AI tool: `tool` on POST /api/lanes/:id/start (a catalog id, known + enabled, else 400 EBADTOOL/EDISABLED); a resume keeps the tool its session ran on (`sessions.tool`). The workspace's `lanes.tool` is now only the default. - The workspace home's prompt box is the one place a session starts. Its fields run in order: AI → AI Router (Native or a profile) → Model (the tool's own list, or the router's models) → Permission (Claude mode / Codex sandbox+approval / catalog bypass) → Effort. - The pane's run form (RunSetup) is gone: New run, a single pane's exit and an empty split pane's New session all go to the home. RunSetup.tsx keeps PromptEditor, CwdAutocomplete and ModelPicker (new `choices` prop). - The tool icon moved from the sidebar's workspace row to each session row (right edge, `session.tool`); the split-pane header shows the run's tool. - An ended session opens in its workspace (`&session=`, SessionReplay.tsx): its conversation read-only plus a prompt box; only a SENT message resumes it (resumeSessionId + prompt, first free slot). The console is only hidden behind it. - Analyst button beside Files/Changes in the right panel opens /sessions/:id for the opened session. - Gemini CLI ships its own icon (client/public/ai-tools/gemini.svg, from zonalogo.com). - RouterPicker always shows (Native disabled for tools without router support); useAiRouters() exported. - Tests: server start-with-tool cases; SessionReplay, home field order/tool/ router models, empty pane, Analyst; dropped the cwd-field suites (Run.defaultCwd, Workspace.laneCwd). Docs: CLAUDE.md rules, API.md, LANES.md, README, ARCHITECTURE, INSTALL.
This commit is contained in:
@@ -3,15 +3,16 @@
|
||||
* sidebar: the ones the workspace OWNS and has not archived
|
||||
* (`GET /api/sessions?lane=<id>&archived=0`, newest first) — never the other
|
||||
* sessions that ran in its folder. A row's dot
|
||||
* shows its state (`sessionDot`: live pane, needs you, error); double-click
|
||||
* shows its state (`sessionDot`: live pane, needs you, error) and its right
|
||||
* edge the AI tool the session ran on (`sessions.tool`); double-click
|
||||
* renames it and its hover button archives it (`PATCH {name}` / `{archived}`,
|
||||
* which deletes nothing). A session with a LIVE pane (a `GET /api/run` handle
|
||||
* carrying its session id) opens that tmux pane on the Workspace page
|
||||
* (`?lane=<id>&run=<runId>`, Ctrl/Cmd+click `&pane=1` to open it in a second
|
||||
* split pane) — attaching, never starting or resuming anything;
|
||||
* any other session opens its read-only transcript (`/sessions/:id`), where
|
||||
* Resume is a separate, explicit action, because a resume re-reads the whole
|
||||
* transcript into context.
|
||||
* any other session opens in its workspace too (`?lane=<id>&session=<id>`,
|
||||
* `SessionReplay`): its conversation with a prompt box, resumed only when a
|
||||
* message is SENT, because a resume re-reads the whole transcript into context.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -23,6 +24,8 @@ import { api, type RunHandle } from "../../lib/api";
|
||||
import { isSessionAwaitingInput, type Lane, type Session } from "../../lib/types";
|
||||
import { timeAgo } from "../../lib/format";
|
||||
import { ConfirmModal } from "../ConfirmModal";
|
||||
import { AiToolIcon } from "../AiToolIcon";
|
||||
import { useAiTools } from "../../hooks/useAiTools";
|
||||
|
||||
export const SESSION_LIMIT = 20;
|
||||
|
||||
@@ -164,6 +167,7 @@ export function WorkspaceSessionList({
|
||||
liveRuns,
|
||||
query,
|
||||
selectedRunId,
|
||||
selectedSessionId = null,
|
||||
onMatches,
|
||||
hidden = false,
|
||||
}: {
|
||||
@@ -174,6 +178,8 @@ export function WorkspaceSessionList({
|
||||
query: string;
|
||||
/** The `run` the Workspace page is attached to, to mark its row. */
|
||||
selectedRunId: string | null;
|
||||
/** The ended session the Workspace page is showing (`session`), likewise. */
|
||||
selectedSessionId?: string | null;
|
||||
/** How many sessions match `query`, once they are loaded — lets the
|
||||
* sidebar search find a workspace by a session's name. */
|
||||
onMatches?: (count: number) => void;
|
||||
@@ -182,6 +188,7 @@ export function WorkspaceSessionList({
|
||||
}) {
|
||||
const { t } = useTranslation("lanes");
|
||||
const navigate = useNavigate();
|
||||
const tools = useAiTools();
|
||||
// Bumped after a rename or an archive, so the list re-reads.
|
||||
const [version, setVersion] = useState(0);
|
||||
const [renaming, setRenaming] = useState<string | null>(null);
|
||||
@@ -276,7 +283,7 @@ export function WorkspaceSessionList({
|
||||
<ul data-testid={`workspace-sessions-${lane.id}`} className="space-y-px">
|
||||
{shown.map((s) => {
|
||||
const run = runFor(s);
|
||||
const selected = !!run && run.id === selectedRunId;
|
||||
const selected = run ? run.id === selectedRunId : s.id === selectedSessionId;
|
||||
if (renaming === s.id) {
|
||||
return (
|
||||
<li key={s.id} className="py-0.5 pl-7 pr-2">
|
||||
@@ -314,7 +321,7 @@ export function WorkspaceSessionList({
|
||||
`/workspace?lane=${lane.id}&run=${encodeURIComponent(run.id)}${
|
||||
e.ctrlKey || e.metaKey ? "&pane=1" : ""
|
||||
}`
|
||||
: `/sessions/${s.id}`
|
||||
: `/workspace?lane=${lane.id}&session=${encodeURIComponent(s.id)}`
|
||||
)
|
||||
}
|
||||
onDoubleClick={() => {
|
||||
@@ -326,7 +333,7 @@ export function WorkspaceSessionList({
|
||||
? `\n${t("tree.worktreeSession", { name: sessionWorktreeName(lane, s) })}`
|
||||
: ""
|
||||
}\n${run ? t("tree.splitHint") : t("tree.transcriptHint")}`}
|
||||
className={`flex w-full items-center gap-2 rounded-md py-1 pl-7 pr-7 text-left text-[12px] ${
|
||||
className={`flex w-full items-center gap-2 rounded-md py-1 pl-7 pr-2 text-left text-[12px] ${
|
||||
selected
|
||||
? "bg-accent/10 text-fg-primary"
|
||||
: "text-fg-secondary hover:bg-surface-3 hover:text-fg-primary"
|
||||
@@ -344,6 +351,10 @@ export function WorkspaceSessionList({
|
||||
className="h-3 w-3 shrink-0 text-fg-muted"
|
||||
/>
|
||||
)}
|
||||
<AiToolIcon
|
||||
tool={{ id: s.tool || "claude", label: tools.label(s.tool || "claude") }}
|
||||
className="ml-auto h-3.5 w-3.5 shrink-0 !p-0 opacity-70 group-hover:invisible"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user