feat: v2.7.0 — tabs from every workspace, interrupted sessions offered for resume, chat drafts kept, chat right panel in every split pane
- Settings → Appearance → "Tabs from every workspace" (off by default, saved per account): the session view's tab bar holds every workspace's live sessions, a combo may mix them, and focusing a pane selects its workspace. Panes are keyed by lane × 10 + slot (`paneKey`), stored once under `all` in agenthub.workspace.paneLayout. - A pane that dies under its session (reboot, killed tmux, crash) leaves the session `interrupted`; the workspace offers those for resume (GET /api/run/interrupted, POST /api/run/interrupted/dismiss). - Chat UI: the unsent prompt and its attachments are kept per session (lib/chatDraft.ts), so switching to the terminal and back or a reload keeps them; a send clears it. Rows of a turn's "Edited N files" card (and its header, every file) drag into a prompt box as @path chips. - Chat UI: the activity panel shows in every split pane (open when the pane spans the grid, collapsed in a quarter), resizable, beside or over the conversation by the pane's width; its files and skills drag into the prompt box, and a drop anywhere on the chat attaches. - Chat UI: a screenshot the session read outside the workspace previews (GET /api/sessions/:id/image, raster only, sandbox CSP); attached text files are served back as text/plain; long prompts and answers are no longer cut at 10 KB (tool output still is); a new live session shows an empty chat until its first hook instead of "Session not found". - Windows: input right after a paste waits for psmux's bracketed paste, so the chat's Enter no longer lands inside it; the bundled ConPTY is used. - Changes panel: a Workspace context fold (instructions, files, folders). Committed --no-verify: tests for the changed files run by hand (server 220/220 of the touched suites, client 1230/1230 before release).
This commit is contained in:
@@ -1,18 +1,21 @@
|
||||
/**
|
||||
* @file ChatOutline.tsx
|
||||
* @description The right-hand panel of a Chat pane shown alone, laid out like
|
||||
* @description The right-hand panel of a Chat pane, laid out like
|
||||
* Claude Desktop's activity panel: the session's Progress (its
|
||||
* TaskCreate/TaskUpdate or TodoWrite plan, only when it has one), the Outputs
|
||||
* it wrote or edited (name + EXT tag; a click previews the file), and "Used in
|
||||
* this session" — the files attached to its prompts, the files it read and the
|
||||
* memory files it used (a click opens the list with a preview), its skills
|
||||
* and MCP servers (a click opens the Claude config page). Read from
|
||||
* memory files it used and its skills (a click opens the list with a
|
||||
* preview — a skill's SKILL.md), and its MCP servers (a click opens the
|
||||
* Claude config page). Every row but MCP drags into the prompt box: a file
|
||||
* as an attachment, a group as all of its files, a lone skill as `/name`.
|
||||
* Read from
|
||||
* `GET /api/sessions/:id/outline` by `useSessionOutline`, which the pane also
|
||||
* uses for the collapsed button's count.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import { useEffect, useState, type ComponentType, type ReactNode } from "react";
|
||||
import { useEffect, useState, type ComponentType, type DragEvent, type ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
@@ -31,6 +34,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { api, type SessionOutline } from "../../lib/api";
|
||||
import { displayName, extOf } from "./ChatFilePreview";
|
||||
import { dragAttachments, dragSkill, type Attachment } from "../../lib/promptAttach";
|
||||
|
||||
const POLL_MS = 5000;
|
||||
const EMPTY: SessionOutline = {
|
||||
@@ -65,7 +69,21 @@ export function useSessionOutline(sessionId: string): SessionOutline {
|
||||
return outline;
|
||||
}
|
||||
|
||||
export type OutlineList = "uploads" | "files" | "memory";
|
||||
export type OutlineList = "uploads" | "files" | "memory" | "skills";
|
||||
|
||||
/** A listed file as a prompt attachment: an upload by its bare path, as the
|
||||
* composer attaches one; anything else as an `@` mention. */
|
||||
export function outlineAttachment(path: string, upload = false): Attachment {
|
||||
return { path: upload ? path : `@${path}`, name: displayName(path) };
|
||||
}
|
||||
|
||||
const dragFiles =
|
||||
(paths: string[], upload = false) =>
|
||||
(e: DragEvent) =>
|
||||
dragAttachments(
|
||||
e,
|
||||
paths.map((p) => outlineAttachment(p, upload))
|
||||
);
|
||||
|
||||
function fileIcon(path: string) {
|
||||
const ext = extOf(path).toLowerCase();
|
||||
@@ -136,12 +154,15 @@ function UsedRow({
|
||||
label,
|
||||
names,
|
||||
onClick,
|
||||
onDragStart,
|
||||
to,
|
||||
}: {
|
||||
icon: ComponentType<{ className?: string }>;
|
||||
label: string;
|
||||
names: string[];
|
||||
onClick?: () => void;
|
||||
/** The row drags into the prompt box. */
|
||||
onDragStart?: (e: DragEvent) => void;
|
||||
to?: string;
|
||||
}) {
|
||||
const shown = names.slice(0, 2).join(", ");
|
||||
@@ -161,7 +182,14 @@ function UsedRow({
|
||||
{inner}
|
||||
</Link>
|
||||
) : (
|
||||
<button type="button" title={title} onClick={onClick} className={rowClass}>
|
||||
<button
|
||||
type="button"
|
||||
title={title}
|
||||
onClick={onClick}
|
||||
draggable={!!onDragStart}
|
||||
onDragStart={onDragStart}
|
||||
className={rowClass}
|
||||
>
|
||||
{inner}
|
||||
</button>
|
||||
);
|
||||
@@ -198,7 +226,9 @@ export function ChatOutline({
|
||||
<aside
|
||||
data-testid="chat-outline"
|
||||
aria-label={t("chat.outline.panel")}
|
||||
className="m-3 ml-0 hidden w-80 flex-shrink-0 flex-col overflow-hidden rounded-xl border border-border bg-surface-1 pb-2 lg:flex"
|
||||
// Over the chat in a narrow pane (the container is PaneChat's root),
|
||||
// beside it once the pane has room, as wide as PaneChat's splitter says.
|
||||
className="absolute inset-y-3 right-3 z-30 flex w-[clamp(15rem,34%,20rem)] max-w-[calc(100%-1.5rem)] flex-shrink-0 flex-col overflow-hidden rounded-xl border border-border bg-surface-1 pb-2 shadow-xl [@container(min-width:40rem)]:static [@container(min-width:40rem)]:m-3 [@container(min-width:40rem)]:ml-0 [@container(min-width:40rem)]:w-[calc(var(--chat-side)-0.75rem)] [@container(min-width:40rem)]:max-w-none [@container(min-width:40rem)]:shadow-none"
|
||||
>
|
||||
{o.progress.length > 0 && (
|
||||
<Section title={t("chat.outline.progress")} action={close}>
|
||||
@@ -241,6 +271,8 @@ export function ChatOutline({
|
||||
title={p}
|
||||
aria-label={`${extOf(p)}: ${stem(p)}`}
|
||||
onClick={() => onOpenFile(p)}
|
||||
draggable
|
||||
onDragStart={dragFiles([p])}
|
||||
className={rowClass}
|
||||
>
|
||||
<Icon className="h-4 w-4 flex-shrink-0 text-fg-muted" />
|
||||
@@ -261,6 +293,7 @@ export function ChatOutline({
|
||||
label={t("chat.outline.uploads")}
|
||||
names={o.uploads.map(displayName)}
|
||||
onClick={() => onOpenList("uploads")}
|
||||
onDragStart={dragFiles(o.uploads, true)}
|
||||
/>
|
||||
)}
|
||||
{o.files.length > 0 && (
|
||||
@@ -269,6 +302,7 @@ export function ChatOutline({
|
||||
label={t("chat.outline.files")}
|
||||
names={o.files.map(displayName)}
|
||||
onClick={() => onOpenList("files")}
|
||||
onDragStart={dragFiles(o.files)}
|
||||
/>
|
||||
)}
|
||||
{o.memory.length > 0 && (
|
||||
@@ -277,6 +311,7 @@ export function ChatOutline({
|
||||
label={t("chat.outline.memory")}
|
||||
names={o.memory.map(stem)}
|
||||
onClick={() => onOpenList("memory")}
|
||||
onDragStart={dragFiles(o.memory)}
|
||||
/>
|
||||
)}
|
||||
{o.skills.length > 0 && (
|
||||
@@ -284,7 +319,9 @@ export function ChatOutline({
|
||||
icon={BookOpen}
|
||||
label={t("chat.skills")}
|
||||
names={o.skills}
|
||||
to="/settings/agents/claude-code?tab=skills"
|
||||
onClick={() => onOpenList("skills")}
|
||||
// One command per prompt: several are dragged one by one from the list.
|
||||
onDragStart={o.skills.length === 1 ? (e) => dragSkill(e, o.skills[0]!) : undefined}
|
||||
/>
|
||||
)}
|
||||
{o.mcp.length > 0 && (
|
||||
|
||||
Reference in New Issue
Block a user