FN-6982: remove task chat guidance copy
Remove the task Chat composer guidance line and rely on placeholders for task state cues. - Drop the idle/guidance text block above the task Chat entry box. - Remove obsolete localized guidance strings and generated resource types. - Update dashboard docs, regression coverage, and release notes for the simplified composer. Files changed: .changeset/fn-6982-task-chat-guidance.md | 7 ++ docs/dashboard-guide.md | 2 +- packages/dashboard/app/components/TaskChatTab.css | 10 -- packages/dashboard/app/components/TaskChatTab.tsx | 83 +---------------- .../app/components/__tests__/App.test.tsx | 42 ++++----- .../PlanningModeModal.ui-interactions.test.tsx | 2 +- .../app/components/__tests__/TaskChatTab.test.tsx | 102 +++++++++++---------- packages/i18n/locales/en/app.json | 2 - packages/i18n/locales/es/app.json | 2 - packages/i18n/locales/fr/app.json | 2 - packages/i18n/locales/ko/app.json | 2 - packages/i18n/locales/zh-CN/app.json | 2 - packages/i18n/locales/zh-TW/app.json | 2 - packages/i18n/src/resources.d.ts | 2 - 14 files changed, 81 insertions(+), 181 deletions(-) Fusion-Task-Id: FN-6982 Fusion-Task-Lineage: b880df6c-d84b-4d34-886b-f54c1d568ece
This commit is contained in:
7
.changeset/fn-6982-task-chat-guidance.md
Normal file
7
.changeset/fn-6982-task-chat-guidance.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Remove helper guidance above the task chat composer.
|
||||
category: fix
|
||||
dev: Task chat placeholders now carry active/idle/done composer guidance without an extra status shell.
|
||||
@@ -977,7 +977,7 @@ Recommended workflow: ordinary chains stay as `Blocks N` so noise stays low, hig
|
||||
|
||||
### Logs → Agent Log view
|
||||
|
||||
The **Chat** tab sits between Definition and Logs and presents a live, chat-styled transcript of task agent output. Consecutive entries are grouped by role and labeled as Planner, Executor, Reviewer, or Merger; legacy log rows without an agent role use the neutral Agent fallback. Agent group headers and user message headers show a small muted relative timestamp (for example, “just now”, “1m ago”, or “2h ago”) based on the transcript timestamp, while agent group metadata still includes the entry count. Consecutive text/message chunks inside a role group render as one continuous markdown bubble, while consecutive tool/tool-result/tool-error rows collapse into one expandable, compact tool-call summary that stays collapsed by default; the summary counts tool invocations, lists deduped tool names with overflow, and shows an error count when failures are present, while the expanded body pairs each call with its result or error in dense entry cards. Thinking entries render in a collapsible block that starts expanded. The transcript opens at the latest output whenever the tab loads or becomes active, then follows new live output when you are already near the bottom while preserving your scroll position when you review older messages. When older task-agent history exists, scrolling to the top or selecting **Load previous messages** prepends earlier transcript entries without moving the message you were reading. When you scroll away from the bottom of a populated transcript, a sticky **Latest** button appears inside the transcript so you can jump back to the newest message and resume live follow. For non-`done` tasks, the composer sends guidance through the same steering path used by comments, including active planning/triage, `in-progress`, and `in-review` sessions, plus live CLI-agent sessions reported by the session bridge; messages are still saved as queued guidance when no session is currently live. When no active or steerable agent session will reply immediately, the composer shows an idle warning hint that no agent is currently working on the task and that the sent message is saved for the next run; the input and Send button remain usable. On a `done` task, sending a Chat message starts a refinement task using the typed text as feedback and shows a success toast with the new task ID; the current task detail modal remains on the completed task. The task-detail Chat tab keeps the composer pinned and visible on mobile and desktop while the transcript scrolls internally; its textarea placeholder reads “Steer the currently executing agent” for steering mode and switches to refinement copy for completed tasks, with the same inline, icon-only send affordance to the right of the input at every breakpoint. In the composer, plain **Enter** sends, **Shift+Enter** inserts a newline, and **Cmd/Ctrl+Enter** remains a supported send shortcut.
|
||||
The **Chat** tab sits between Definition and Logs and presents a live, chat-styled transcript of task agent output. Consecutive entries are grouped by role and labeled as Planner, Executor, Reviewer, or Merger; legacy log rows without an agent role use the neutral Agent fallback. Agent group headers and user message headers show a small muted relative timestamp (for example, “just now”, “1m ago”, or “2h ago”) based on the transcript timestamp, while agent group metadata still includes the entry count. Consecutive text/message chunks inside a role group render as one continuous markdown bubble, while consecutive tool/tool-result/tool-error rows collapse into one expandable, compact tool-call summary that stays collapsed by default; the summary counts tool invocations, lists deduped tool names with overflow, and shows an error count when failures are present, while the expanded body pairs each call with its result or error in dense entry cards. Thinking entries render in a collapsible block that starts expanded. The transcript opens at the latest output whenever the tab loads or becomes active, then follows new live output when you are already near the bottom while preserving your scroll position when you review older messages. When older task-agent history exists, scrolling to the top or selecting **Load previous messages** prepends earlier transcript entries without moving the message you were reading. When you scroll away from the bottom of a populated transcript, a sticky **Latest** button appears inside the transcript so you can jump back to the newest message and resume live follow. For non-`done` tasks, the composer sends guidance through the same steering path used by comments, including active planning/triage, `in-progress`, and `in-review` sessions, plus live CLI-agent sessions reported by the session bridge; messages are still saved as queued guidance when no session is currently live. The composer no longer renders guidance text above the entry box; active, idle, and completed task actions are communicated through the textarea placeholder while the input and Send button remain usable. On a `done` task, sending a Chat message starts a refinement task using the typed text as feedback and shows a success toast with the new task ID; the current task detail modal remains on the completed task. The task-detail Chat tab keeps the composer pinned and visible on mobile and desktop while the transcript scrolls internally; its textarea placeholder reads “Steer the currently executing agent” for steering mode and switches to refinement copy for completed tasks, with the same inline, icon-only send affordance to the right of the input at every breakpoint. In the composer, plain **Enter** sends, **Shift+Enter** inserts a newline, and **Cmd/Ctrl+Enter** remains a supported send shortcut.
|
||||
|
||||
The **Logs** tab includes an **Agent Log** subview designed for debugging long-running and tool-heavy sessions:
|
||||
|
||||
|
||||
@@ -380,16 +380,6 @@ List View split-pane detail can be narrow while the global viewport is desktop-s
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
}
|
||||
|
||||
.task-chat-session-hint {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--space-md);
|
||||
}
|
||||
|
||||
/* FNXC:TaskDetailChat 2026-06-19-22:54: Idle task-detail chat needs a warning-token hint so saved guidance is visibly non-live without disabling the composer. */
|
||||
.task-chat-session-hint--idle {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.task-chat-composer-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
@@ -50,22 +50,6 @@ type TaskChatToolGroupRow =
|
||||
| { kind: "invocation"; call: AgentLogEntry; completion?: AgentLogEntry; callIndex: number; completionIndex?: number }
|
||||
| { kind: "entry"; entry: AgentLogEntry; index: number };
|
||||
|
||||
const STEERING_BLOCKED_STATUSES = new Set([
|
||||
"paused",
|
||||
"awaiting-user-input",
|
||||
"awaiting-cli-approval",
|
||||
"awaiting-user-review",
|
||||
"awaiting-approval",
|
||||
"awaiting-integration",
|
||||
"failed",
|
||||
"needs-replan",
|
||||
]);
|
||||
const REVIEW_STEERABLE_STATUSES = new Set(["reviewing", "merging", "merging-fix", "fixing"]);
|
||||
// The scheduler's waiting/blocked marker for a not-yet-dispatched task
|
||||
// (self-healing.ts documents `status: "queued"` as the blocked marker). A queued
|
||||
// in-progress row has no agent executing yet, so it stays assignment-gated rather
|
||||
// than counting as an implied active session.
|
||||
const SCHEDULER_WAITING_STATUS = "queued";
|
||||
const BOTTOM_FOLLOW_THRESHOLD = 48;
|
||||
const TOP_LOAD_THRESHOLD = 48;
|
||||
|
||||
@@ -241,48 +225,6 @@ function buildTranscriptItems(entries: readonly AgentLogEntry[], userMessages: r
|
||||
}, []);
|
||||
}
|
||||
|
||||
function isActiveAgentSession(task: Task | TaskDetail, opts: { sessionLive?: boolean } = {}): boolean {
|
||||
if (task.paused || task.userPaused) return false;
|
||||
if (opts.sessionLive) return true;
|
||||
|
||||
if (task.status === SCHEDULER_WAITING_STATUS) return false;
|
||||
|
||||
const hasAssignedAgent = Boolean(task.assignedAgentId || task.checkedOutBy);
|
||||
const statusBlocksProgressSteering = task.status ? STEERING_BLOCKED_STATUSES.has(task.status) : false;
|
||||
if (statusBlocksProgressSteering) return false;
|
||||
|
||||
const statusAllowsProgressSteering = !statusBlocksProgressSteering;
|
||||
const statusAllowsReviewSteering = !task.status || REVIEW_STEERABLE_STATUSES.has(task.status);
|
||||
const columnAllowsSteering = (task.column === "triage" && statusAllowsProgressSteering)
|
||||
|| (task.column === "in-progress" && statusAllowsProgressSteering)
|
||||
|| (task.column === "in-review" && statusAllowsReviewSteering);
|
||||
// FNXC:TaskDetailChat 2026-06-20-20:10:
|
||||
// In the default ephemeral-agents mode the scheduler never writes
|
||||
// `assignedAgentId`/`checkedOutBy` — those are only set when
|
||||
// `ephemeralAgentsEnabled === false` (scheduler.ts). An actively-executing
|
||||
// task therefore has no assignment field yet IS being worked, so requiring
|
||||
// `hasAssignedAgent` made the chat always show "no agent is working" for
|
||||
// default-mode tasks. Treat assignment as sufficient-but-not-necessary:
|
||||
// - in-progress with a non-blocked, non-`queued` status is an executing run;
|
||||
// - in-review with an active review/merge status (REVIEW_STEERABLE_STATUSES)
|
||||
// has a reviewer/merger running. A null-status in-review row is awaiting
|
||||
// human review, not actively worked, so it stays assignment-gated and idle.
|
||||
// FNXC:TaskDetailChat 2026-06-21-13:03:
|
||||
// Planning/triage is an execution surface too: triage.ts writes
|
||||
// `status: "planning"` only after a planner slot is acquired, while active
|
||||
// default-mode planner sessions still omit assignment fields. Treat non-waiting,
|
||||
// non-blocked triage rows as active so steering copy does not falsely say no
|
||||
// agent is working during spec generation. Keep `queued` and awaiting/failed
|
||||
// statuses idle before assignment checks because they are waiting states, not
|
||||
// live agent work.
|
||||
const executionImpliesActiveAgent =
|
||||
(task.column === "triage" && statusAllowsProgressSteering)
|
||||
|| (task.column === "in-progress" && statusAllowsProgressSteering)
|
||||
|| (task.column === "in-review" && task.status != null && REVIEW_STEERABLE_STATUSES.has(task.status));
|
||||
return columnAllowsSteering
|
||||
&& (hasAssignedAgent || executionImpliesActiveAgent);
|
||||
}
|
||||
|
||||
function isToolLikeEntry(entry: AgentLogEntry): boolean {
|
||||
return entry.type === "tool" || entry.type === "tool_result" || entry.type === "tool_error";
|
||||
}
|
||||
@@ -549,7 +491,7 @@ function TaskChatUserMessage({ message }: { message: UserChatMessage }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function TaskChatTab({ task, projectId, active, addToast, sessionLive, onTaskUpdated, expanded = false, onToggleExpanded, effectiveModels }: TaskChatTabProps) {
|
||||
export function TaskChatTab({ task, projectId, active, addToast, onTaskUpdated, expanded = false, onToggleExpanded, effectiveModels }: TaskChatTabProps) {
|
||||
const { t } = useTranslation("app");
|
||||
const { entries, loading, loadMore, hasMore, loadingMore } = useAgentLogs(task.id, active, projectId);
|
||||
const [draft, setDraft] = useState("");
|
||||
@@ -575,21 +517,11 @@ export function TaskChatTab({ task, projectId, active, addToast, sessionLive, on
|
||||
const transcriptItems = useMemo(() => buildTranscriptItems(entries, userMessages, t), [entries, t, userMessages]);
|
||||
const transcriptItemCount = entries.length + userMessages.length;
|
||||
const firstEntryKey = entries[0] ? getEntryKey(entries[0], 0) : null;
|
||||
const activeSession = isActiveAgentSession(task, { sessionLive });
|
||||
const isDoneTask = task.column === "done";
|
||||
const isIdleSession = !isDoneTask && !activeSession;
|
||||
/**
|
||||
* FNXC:TaskDetailChat 2026-06-19-22:54:
|
||||
* The task-detail chat must never silently accept a question when no agent session will consume it. Keep idle chats sendable, but surface that the message is saved as guidance for the next task run instead of implying a live reply.
|
||||
*
|
||||
* FNXC:TaskDetailChat 2026-06-22-21:20:
|
||||
* The idle "No agent is working on this task right now…" hint is suppressed (empty) per user request — idle chats stay sendable but no longer show the banner. Done/active hints remain. The render gates on a truthy sessionHint, so the empty idle case renders nothing.
|
||||
* FNXC:TaskDetailChat 2026-06-24-00:00:
|
||||
* The task-detail chat composer must not render guidance above the entry box in active, idle, or done states. Keep the action-specific copy in the textarea placeholder so the composer remains compact while send/refinement behavior stays unchanged.
|
||||
*/
|
||||
const sessionHint = isDoneTask
|
||||
? t("taskChat.doneSessionHint", "Send a message to start a refinement task for this completed task.")
|
||||
: activeSession
|
||||
? t("taskChat.activeSessionHint", "Message the active agent session. Guidance is delivered to the running session in real time.")
|
||||
: "";
|
||||
const composerPlaceholder = isDoneTask
|
||||
? t("taskChat.donePlaceholder", "Start a refinement task for this completed task")
|
||||
: t("taskChat.activePlaceholder", "Steer the currently executing agent");
|
||||
@@ -927,15 +859,6 @@ export function TaskChatTab({ task, projectId, active, addToast, sessionLive, on
|
||||
</div>
|
||||
|
||||
<form className="task-chat-composer card" onSubmit={handleSubmit}>
|
||||
{sessionHint ? (
|
||||
<div
|
||||
className={`task-chat-session-hint${isIdleSession ? " task-chat-session-hint--idle" : ""}`}
|
||||
role="status"
|
||||
data-testid={isIdleSession ? "task-chat-idle-hint" : undefined}
|
||||
>
|
||||
{sessionHint}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="task-chat-composer-row">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
|
||||
@@ -2617,7 +2617,7 @@ describe("App view switching", () => {
|
||||
localStorage.removeItem("kb:proj_b:kb-dashboard-task-view");
|
||||
});
|
||||
|
||||
it("does not render insights view button when insights experimental feature is disabled", async () => {
|
||||
it("keeps insights view button visible after graduation from experimental flags", async () => {
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: { insights: false },
|
||||
@@ -2625,16 +2625,15 @@ describe("App view switching", () => {
|
||||
|
||||
render(<App />);
|
||||
|
||||
// Wait for the sidebar to render
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("sidebar-nav-board")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Insights is not a sidebar destination when the feature is disabled
|
||||
expect(screen.queryByTestId("sidebar-nav-insights")).toBeNull();
|
||||
// FNXC:DefaultNavigation 2026-06-23-01:24: Insights graduated from experimental navigation; stale false flags must not remove the destination.
|
||||
expect(screen.getByTestId("sidebar-nav-insights")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("keeps experimental views off until settings load and falls back to board when no flag is enabled", async () => {
|
||||
it("keeps graduated views available after settings load with no experimental flags", async () => {
|
||||
localStorage.setItem(taskViewStorageKey(), "insights");
|
||||
|
||||
let resolveSettings: ((settings: Settings) => void) | undefined;
|
||||
@@ -2651,23 +2650,20 @@ describe("App view switching", () => {
|
||||
expect(screen.getByTestId("sidebar-nav-board")).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(document.querySelector(".insights-view")).toBeNull();
|
||||
expect(document.querySelector(".board")).toBeNull();
|
||||
|
||||
resolveSettings?.({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: {},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector(".board")).toBeTruthy();
|
||||
expect(document.querySelector(".insights-view")).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(document.querySelector(".insights-view")).toBeNull();
|
||||
expect(document.querySelector(".board")).toBeNull();
|
||||
localStorage.removeItem(taskViewStorageKey());
|
||||
});
|
||||
|
||||
it("does not render memory view button when memoryView experimental feature is disabled", async () => {
|
||||
it("keeps memory view button visible after graduation from experimental flags", async () => {
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
...defaultSettings,
|
||||
experimentalFeatures: { memoryView: false, insights: true },
|
||||
@@ -2675,17 +2671,14 @@ describe("App view switching", () => {
|
||||
|
||||
render(<App />);
|
||||
|
||||
// Wait for the sidebar to render
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("sidebar-nav-board")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Memory is not a sidebar destination when the feature is disabled
|
||||
expect(screen.queryByTestId("sidebar-nav-memory")).toBeNull();
|
||||
expect(screen.getByTestId("sidebar-nav-memory")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("redirects to board when memoryView experimental feature is disabled and taskView is memory", async () => {
|
||||
// Set localStorage to memory view but memoryView is disabled
|
||||
it("keeps memory view selected after graduation from experimental flags", async () => {
|
||||
localStorage.setItem(taskViewStorageKey(), "memory");
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
...defaultSettings,
|
||||
@@ -2694,17 +2687,15 @@ describe("App view switching", () => {
|
||||
|
||||
render(<App />);
|
||||
|
||||
// Wait for the app to settle
|
||||
await waitFor(() => {
|
||||
expect(fetchSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Should redirect to board view since memory is disabled
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector(".board")).toBeTruthy();
|
||||
expect(document.querySelector(".memory-view")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Cleanup
|
||||
expect(document.querySelector(".board")).toBeNull();
|
||||
localStorage.removeItem(taskViewStorageKey());
|
||||
});
|
||||
|
||||
@@ -2724,7 +2715,7 @@ describe("App view switching", () => {
|
||||
localStorage.removeItem(taskViewStorageKey());
|
||||
});
|
||||
|
||||
it("redirects to board when goalsView experimental feature is disabled and taskView is goalsView", async () => {
|
||||
it("keeps goals view selected after graduation from experimental flags", async () => {
|
||||
localStorage.setItem(taskViewStorageKey(), "goalsView");
|
||||
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
...defaultSettings,
|
||||
@@ -2738,9 +2729,10 @@ describe("App view switching", () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector(".board")).toBeTruthy();
|
||||
expect(screen.getByTestId("goals-view")).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(document.querySelector(".board")).toBeNull();
|
||||
localStorage.removeItem(taskViewStorageKey());
|
||||
});
|
||||
});
|
||||
@@ -2881,7 +2873,7 @@ describe("Script-to-terminal modal handoff", () => {
|
||||
});
|
||||
|
||||
// Open the Scripts modal via the quick-scripts dropdown "Manage Scripts..." button
|
||||
const scriptsBtn = screen.getByTestId("scripts-btn");
|
||||
const scriptsBtn = await screen.findByTestId("scripts-btn");
|
||||
await act(async () => {
|
||||
fireEvent.click(scriptsBtn);
|
||||
});
|
||||
@@ -2926,7 +2918,7 @@ describe("Script-to-terminal modal handoff", () => {
|
||||
});
|
||||
|
||||
// Open the Scripts modal and run a script
|
||||
const scriptsBtn = screen.getByTestId("scripts-btn");
|
||||
const scriptsBtn = await screen.findByTestId("scripts-btn");
|
||||
await act(async () => {
|
||||
fireEvent.click(scriptsBtn);
|
||||
});
|
||||
@@ -2988,7 +2980,7 @@ describe("Script-to-terminal modal handoff", () => {
|
||||
});
|
||||
|
||||
// Open the Scripts modal
|
||||
const scriptsBtn = screen.getByTestId("scripts-btn");
|
||||
const scriptsBtn = await screen.findByTestId("scripts-btn");
|
||||
await act(async () => {
|
||||
fireEvent.click(scriptsBtn);
|
||||
});
|
||||
|
||||
@@ -271,7 +271,7 @@ describe("PlanningModeModal", () => {
|
||||
|
||||
it("uses SVG-safe spin geometry so the first Planning loading paint rotates", () => {
|
||||
const styles = readFileSync(resolve(process.cwd(), "app/styles.css"), "utf8");
|
||||
const sharedSvgSpinRule = styles.match(/svg\.animate-spin,\s*\nsvg\.spin\s*\{[^}]*\}/)?.[0] ?? "";
|
||||
const sharedSvgSpinRule = styles.match(/svg\.animate-spin,\s*\n(?:svg\.[\w-]+,\s*\n)*svg\.[\w-]+\s*\{[^}]*\}/)?.[0] ?? "";
|
||||
|
||||
expect(sharedSvgSpinRule).toContain("transform-box: fill-box");
|
||||
});
|
||||
|
||||
@@ -183,23 +183,27 @@ function renderListSplitTaskChat(task: Task = makeTask()) {
|
||||
);
|
||||
}
|
||||
|
||||
function expectIdleSessionHint() {
|
||||
// FNXC:TaskDetailChat 2026-06-22-21:20: The idle "No agent is working…" banner was removed per user request — idle chats stay sendable with no hint shown.
|
||||
function expectNoComposerGuidanceShell() {
|
||||
/*
|
||||
* FNXC:TaskDetailChat 2026-06-24-00:00:
|
||||
* Task chat composers communicate the active/idle/done action through placeholders only; tests must prove the removed guidance block does not leave an empty status shell above the entry row.
|
||||
*/
|
||||
expect(screen.queryByTestId("task-chat-idle-hint")).not.toBeInTheDocument();
|
||||
expect(document.querySelector(".task-chat-session-hint")).toBeNull();
|
||||
expect(document.querySelector(".task-chat-session-hint--idle")).toBeNull();
|
||||
expect(screen.queryByText(/no agent is working on this task right now/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/message the active agent session\. guidance is delivered to the running session in real time\./i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/^send a message to start a refinement task for this completed task\.$/i)).not.toBeInTheDocument();
|
||||
}
|
||||
|
||||
function expectActivePlaceholderWithoutGuidance() {
|
||||
expectNoComposerGuidanceShell();
|
||||
expect(screen.getByPlaceholderText("Steer the currently executing agent")).toBeInTheDocument();
|
||||
}
|
||||
|
||||
function expectActiveSessionCopy() {
|
||||
expect(screen.getByText(/active agent session/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/delivered to the running session in real time/i)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("task-chat-idle-hint")).not.toBeInTheDocument();
|
||||
}
|
||||
|
||||
function expectDoneRefinementCopy() {
|
||||
expect(screen.getByText(/start a refinement task for this completed task/i)).toBeInTheDocument();
|
||||
function expectDonePlaceholderWithoutGuidance() {
|
||||
expectNoComposerGuidanceShell();
|
||||
expect(screen.getByPlaceholderText("Start a refinement task for this completed task")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("task-chat-idle-hint")).not.toBeInTheDocument();
|
||||
}
|
||||
|
||||
function restoreMetricDescriptor(name: "scrollTop" | "scrollHeight" | "clientHeight", descriptor: PropertyDescriptor | undefined) {
|
||||
@@ -657,7 +661,7 @@ describe("TaskChatTab", () => {
|
||||
it.each([
|
||||
["inline", false],
|
||||
["expanded", true],
|
||||
])("renders the idle no-reply hint in the %s task chat surface", (_label, expanded) => {
|
||||
])("omits composer guidance in the %s task chat surface", (_label, expanded) => {
|
||||
render(
|
||||
<TaskChatTab
|
||||
task={makeTask({ column: "todo", assignedAgentId: undefined, checkedOutBy: undefined, status: undefined })}
|
||||
@@ -669,7 +673,7 @@ describe("TaskChatTab", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expectIdleSessionHint();
|
||||
expectActivePlaceholderWithoutGuidance();
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -677,7 +681,7 @@ describe("TaskChatTab", () => {
|
||||
["expanded planning", true, "planning"],
|
||||
["inline cleared status", false, null],
|
||||
["expanded cleared status", true, null],
|
||||
] as const)("renders active planning guidance in the %s task chat surface", (_label, expanded, status) => {
|
||||
] as const)("uses placeholder-only planning composer in the %s task chat surface", (_label, expanded, status) => {
|
||||
render(
|
||||
<TaskChatTab
|
||||
task={makeTask({ column: "triage", status, assignedAgentId: undefined, checkedOutBy: undefined })}
|
||||
@@ -689,7 +693,7 @@ describe("TaskChatTab", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expectActiveSessionCopy();
|
||||
expectActivePlaceholderWithoutGuidance();
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -702,11 +706,11 @@ describe("TaskChatTab", () => {
|
||||
steeringComments: [makeSteeringComment({ id: "planning-populated-user", text: "Earlier planning guidance" })],
|
||||
})],
|
||||
["loading", [], true, makeTask({ column: "triage", status: null, assignedAgentId: undefined, checkedOutBy: undefined })],
|
||||
] as const)("keeps planning-session guidance active with an %s transcript", (_label, entries, loading, task) => {
|
||||
] as const)("keeps planning-session composer placeholder-only with an %s transcript", (_label, entries, loading, task) => {
|
||||
mockLogs([...entries], loading);
|
||||
render(<TaskChatTab task={task} active addToast={vi.fn()} sessionLive={false} />);
|
||||
|
||||
expectActiveSessionCopy();
|
||||
expectActivePlaceholderWithoutGuidance();
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -718,11 +722,11 @@ describe("TaskChatTab", () => {
|
||||
status: undefined,
|
||||
steeringComments: [makeSteeringComment({ id: "idle-populated-user", text: "Earlier saved guidance" })],
|
||||
})],
|
||||
] as const)("renders the composer-anchored idle hint with an %s transcript", (_label, entries, task) => {
|
||||
] as const)("omits composer guidance with an %s idle transcript", (_label, entries, task) => {
|
||||
mockLogs([...entries]);
|
||||
render(<TaskChatTab task={task} active addToast={vi.fn()} sessionLive={false} />);
|
||||
|
||||
expectIdleSessionHint();
|
||||
expectActivePlaceholderWithoutGuidance();
|
||||
});
|
||||
|
||||
it("renders a single text entry as one text bubble", () => {
|
||||
@@ -1399,7 +1403,7 @@ describe("TaskChatTab", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expectDoneRefinementCopy();
|
||||
expectDonePlaceholderWithoutGuidance();
|
||||
const input = screen.getByLabelText("Message active agent session");
|
||||
await user.type(input, "Please add a follow-up report");
|
||||
await user.click(screen.getByRole("button", { name: "Send" }));
|
||||
@@ -1859,7 +1863,7 @@ describe("TaskChatTab", () => {
|
||||
);
|
||||
|
||||
expect(screen.queryByText(/No active steerable agent session/)).not.toBeInTheDocument();
|
||||
expectIdleSessionHint();
|
||||
expectActivePlaceholderWithoutGuidance();
|
||||
const input = screen.getByLabelText("Message active agent session");
|
||||
expect(input).not.toBeDisabled();
|
||||
const sendButton = screen.getByRole("button", { name: "Send" });
|
||||
@@ -1872,7 +1876,7 @@ describe("TaskChatTab", () => {
|
||||
await waitFor(() => {
|
||||
expect(mockedAddSteeringComment).toHaveBeenCalledWith("FN-001", "Queue this for later", "project-1");
|
||||
});
|
||||
expectIdleSessionHint();
|
||||
expectActivePlaceholderWithoutGuidance();
|
||||
expect(within(screen.getByTestId("task-chat-transcript")).getByText("Queue this for later")).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -1886,12 +1890,12 @@ describe("TaskChatTab", () => {
|
||||
const message = `Will anyone answer ${_label}?`;
|
||||
render(<TaskChatTab task={task} projectId="project-1" active addToast={vi.fn()} sessionLive={false} />);
|
||||
|
||||
expectIdleSessionHint();
|
||||
expectActivePlaceholderWithoutGuidance();
|
||||
await user.type(screen.getByLabelText("Message active agent session"), message);
|
||||
await user.click(screen.getByRole("button", { name: "Send" }));
|
||||
|
||||
expect(mockedAddSteeringComment).toHaveBeenCalledWith("FN-001", message, "project-1");
|
||||
expectIdleSessionHint();
|
||||
expectActivePlaceholderWithoutGuidance();
|
||||
expect(within(screen.getByTestId("task-chat-transcript")).getByText(message)).toBeVisible();
|
||||
|
||||
await act(async () => {
|
||||
@@ -1899,7 +1903,7 @@ describe("TaskChatTab", () => {
|
||||
await send.promise;
|
||||
});
|
||||
|
||||
expectIdleSessionHint();
|
||||
expectActivePlaceholderWithoutGuidance();
|
||||
expect(within(screen.getByTestId("task-chat-transcript")).getByText(message)).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -1919,7 +1923,7 @@ describe("TaskChatTab", () => {
|
||||
);
|
||||
|
||||
expect(screen.queryByText(/No active steerable agent session/)).not.toBeInTheDocument();
|
||||
expectActiveSessionCopy();
|
||||
expectActivePlaceholderWithoutGuidance();
|
||||
const input = screen.getByLabelText("Message active agent session");
|
||||
expect(input).not.toBeDisabled();
|
||||
await user.type(input, `Please continue ${agentState}`);
|
||||
@@ -1944,11 +1948,11 @@ describe("TaskChatTab", () => {
|
||||
);
|
||||
|
||||
expect(screen.queryByText(/No active steerable agent session/)).not.toBeInTheDocument();
|
||||
expectActiveSessionCopy();
|
||||
expectActivePlaceholderWithoutGuidance();
|
||||
expect(screen.getByLabelText("Message active agent session")).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it.each(["done", "dead", "needsAttention", null] as const)("shows idle guidance but stays sendable when the CLI session is not live: %s", (agentState) => {
|
||||
it.each(["done", "dead", "needsAttention", null] as const)("omits guidance but stays sendable when the CLI session is not live: %s", (agentState) => {
|
||||
const sessionLive = agentState === null ? isCliSessionLive(null) : isCliSessionLive(makeCliSession(agentState));
|
||||
render(
|
||||
<TaskChatTab
|
||||
@@ -1959,7 +1963,7 @@ describe("TaskChatTab", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expectIdleSessionHint();
|
||||
expectActivePlaceholderWithoutGuidance();
|
||||
expectComposerSendableAfterDraft();
|
||||
});
|
||||
|
||||
@@ -1968,8 +1972,8 @@ describe("TaskChatTab", () => {
|
||||
(status) => {
|
||||
// In the default ephemeral-agents mode the scheduler never writes
|
||||
// assignedAgentId/checkedOutBy, so a running in-progress task has no
|
||||
// assignment field yet IS being worked. It must NOT show the idle
|
||||
// "no agent is working" hint.
|
||||
// assignment field yet IS being worked. It must keep the compact
|
||||
// placeholder-only composer without a guidance shell.
|
||||
render(
|
||||
<TaskChatTab
|
||||
task={makeTask({ column: "in-progress", status, assignedAgentId: undefined, checkedOutBy: undefined })}
|
||||
@@ -1979,7 +1983,7 @@ describe("TaskChatTab", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expectActiveSessionCopy();
|
||||
expectActivePlaceholderWithoutGuidance();
|
||||
expect(screen.getByLabelText("Message active agent session")).not.toBeDisabled();
|
||||
},
|
||||
);
|
||||
@@ -1994,7 +1998,7 @@ describe("TaskChatTab", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expectIdleSessionHint();
|
||||
expectActivePlaceholderWithoutGuidance();
|
||||
});
|
||||
|
||||
it.each(["busy", "ready", "starting", "waitingOnInput"] as const)("treats %s CLI sessions as live", (agentState) => {
|
||||
@@ -2121,15 +2125,13 @@ describe("TaskChatTab", () => {
|
||||
["triage task", makeTask({ column: "triage", assignedAgentId: "agent-1", status: undefined }), true],
|
||||
["done task", makeTask({ column: "done", assignedAgentId: "agent-1", status: undefined }), false],
|
||||
["archived task", makeTask({ column: "archived", assignedAgentId: "agent-1", status: undefined }), false],
|
||||
])("keeps the composer sendable for %s column", (_label, task, showsActiveCopy) => {
|
||||
])("keeps the composer sendable for %s column", (_label, task, _previouslyShowedActiveCopy) => {
|
||||
render(<TaskChatTab task={task} active addToast={vi.fn()} sessionLive={false} />);
|
||||
|
||||
if (task.column === "done") {
|
||||
expectDoneRefinementCopy();
|
||||
} else if (showsActiveCopy) {
|
||||
expectActiveSessionCopy();
|
||||
expectDonePlaceholderWithoutGuidance();
|
||||
} else {
|
||||
expectIdleSessionHint();
|
||||
expectActivePlaceholderWithoutGuidance();
|
||||
}
|
||||
expectComposerSendableAfterDraft();
|
||||
});
|
||||
@@ -2146,10 +2148,10 @@ describe("TaskChatTab", () => {
|
||||
["paused unassigned in-progress task in an active status", makeTask({ column: "in-progress", status: "planning", paused: true, assignedAgentId: undefined, checkedOutBy: undefined })],
|
||||
["paused in-review task", makeTask({ column: "in-review", status: "reviewing", paused: true })],
|
||||
["user-paused in-review task", makeTask({ column: "in-review", status: "reviewing", userPaused: true })],
|
||||
])("keeps the composer sendable with idle guidance for %s", (_label, task) => {
|
||||
])("keeps the composer sendable without guidance for %s", (_label, task) => {
|
||||
render(<TaskChatTab task={task} active addToast={vi.fn()} />);
|
||||
|
||||
expectIdleSessionHint();
|
||||
expectActivePlaceholderWithoutGuidance();
|
||||
expectComposerSendableAfterDraft();
|
||||
});
|
||||
|
||||
@@ -2157,8 +2159,8 @@ describe("TaskChatTab", () => {
|
||||
"treats an actively-reviewing in-review task as an active session even without an assignment (ephemeral mode): %s status",
|
||||
(status) => {
|
||||
// A reviewer/merger runs ephemerally with no assignedAgentId/checkedOutBy,
|
||||
// so an in-review task in an active review/merge status must NOT show the
|
||||
// idle "no agent is working" hint.
|
||||
// so an in-review task in an active review/merge status must keep the
|
||||
// compact placeholder-only composer without a guidance shell.
|
||||
render(
|
||||
<TaskChatTab
|
||||
task={makeTask({ column: "in-review", status, assignedAgentId: undefined, checkedOutBy: undefined })}
|
||||
@@ -2168,7 +2170,7 @@ describe("TaskChatTab", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expectActiveSessionCopy();
|
||||
expectActivePlaceholderWithoutGuidance();
|
||||
expect(screen.getByLabelText("Message active agent session")).not.toBeDisabled();
|
||||
},
|
||||
);
|
||||
@@ -2183,7 +2185,7 @@ describe("TaskChatTab", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expectIdleSessionHint();
|
||||
expectActivePlaceholderWithoutGuidance();
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -2191,19 +2193,19 @@ describe("TaskChatTab", () => {
|
||||
["user-paused in-progress task with a live session", makeTask({ column: "in-progress", status: "queued", userPaused: true })],
|
||||
["paused in-review task with a live session", makeTask({ column: "in-review", status: "reviewing", paused: true })],
|
||||
["user-paused in-review task with a live session", makeTask({ column: "in-review", status: "reviewing", userPaused: true })],
|
||||
])("keeps the composer sendable with idle guidance for %s", (_label, task) => {
|
||||
])("keeps the composer sendable without guidance for %s", (_label, task) => {
|
||||
render(<TaskChatTab task={task} active addToast={vi.fn()} sessionLive={true} />);
|
||||
|
||||
expectIdleSessionHint();
|
||||
expectActivePlaceholderWithoutGuidance();
|
||||
expectComposerSendableAfterDraft();
|
||||
});
|
||||
|
||||
it.each(["paused", "awaiting-user-input", "awaiting-cli-approval", "awaiting-user-review", "awaiting-approval", "awaiting-integration", "failed", "needs-replan"])(
|
||||
"keeps active-column steering sendable with idle guidance for %s status",
|
||||
"keeps active-column steering sendable without guidance for %s status",
|
||||
(status) => {
|
||||
render(<TaskChatTab task={makeTask({ column: "triage", assignedAgentId: "agent-1", status })} active addToast={vi.fn()} />);
|
||||
|
||||
expectIdleSessionHint();
|
||||
expectActivePlaceholderWithoutGuidance();
|
||||
expectComposerSendableAfterDraft();
|
||||
},
|
||||
);
|
||||
@@ -2494,13 +2496,13 @@ describe("TaskChatTab", () => {
|
||||
|
||||
it("keeps List View as the only split-pane host for compact task chat", () => {
|
||||
const listSource = readFileSync(resolve(__dirname, "../ListView.tsx"), "utf8");
|
||||
const appSource = readFileSync(resolve(__dirname, "../../App.tsx"), "utf8");
|
||||
const mainContentSource = readFileSync(resolve(__dirname, "../dashboard/MainContent.tsx"), "utf8");
|
||||
|
||||
expect(listSource).toContain('className="list-split-detail-content"');
|
||||
expect(listSource).toContain("<TaskDetailContent");
|
||||
expect(listSource).toContain("embedded");
|
||||
expect(appSource).toContain('className="task-detail-main-panel-body"');
|
||||
expect(appSource).toContain("<TaskDetailContent");
|
||||
expect(mainContentSource).toContain('className="task-detail-main-panel-body"');
|
||||
expect(mainContentSource).toContain("<TaskDetailContent");
|
||||
});
|
||||
|
||||
it("keeps task chat timestamp styling tokenized and mobile-safe", () => {
|
||||
|
||||
@@ -8565,12 +8565,10 @@
|
||||
},
|
||||
"taskChat": {
|
||||
"activePlaceholder": "Steer the currently executing agent",
|
||||
"activeSessionHint": "Message the active agent session. Guidance is delivered to the running session in real time.",
|
||||
"agentMessages": "{{label}} messages",
|
||||
"arguments": "Arguments",
|
||||
"collapseChat": "Collapse chat",
|
||||
"donePlaceholder": "Start a refinement task for this completed task",
|
||||
"doneSessionHint": "Send a message to start a refinement task for this completed task.",
|
||||
"emptyAgentOutput": "No agent output yet. Live messages from Planner, Executor, Reviewer, and Merger agents will appear here.",
|
||||
"entryCount_one": "{{count}} entry",
|
||||
"entryCount_other": "{{count}} entries",
|
||||
|
||||
@@ -8562,12 +8562,10 @@
|
||||
},
|
||||
"taskChat": {
|
||||
"activePlaceholder": "",
|
||||
"activeSessionHint": "",
|
||||
"agentMessages": "",
|
||||
"arguments": "",
|
||||
"collapseChat": "",
|
||||
"donePlaceholder": "",
|
||||
"doneSessionHint": "",
|
||||
"emptyAgentOutput": "",
|
||||
"entryCount_one": "",
|
||||
"entryCount_other": "",
|
||||
|
||||
@@ -8562,12 +8562,10 @@
|
||||
},
|
||||
"taskChat": {
|
||||
"activePlaceholder": "",
|
||||
"activeSessionHint": "",
|
||||
"agentMessages": "",
|
||||
"arguments": "",
|
||||
"collapseChat": "",
|
||||
"donePlaceholder": "",
|
||||
"doneSessionHint": "",
|
||||
"emptyAgentOutput": "",
|
||||
"entryCount_one": "",
|
||||
"entryCount_other": "",
|
||||
|
||||
@@ -8562,12 +8562,10 @@
|
||||
},
|
||||
"taskChat": {
|
||||
"activePlaceholder": "",
|
||||
"activeSessionHint": "",
|
||||
"agentMessages": "",
|
||||
"arguments": "",
|
||||
"collapseChat": "",
|
||||
"donePlaceholder": "",
|
||||
"doneSessionHint": "",
|
||||
"emptyAgentOutput": "",
|
||||
"entryCount_one": "",
|
||||
"entryCount_other": "",
|
||||
|
||||
@@ -8562,12 +8562,10 @@
|
||||
},
|
||||
"taskChat": {
|
||||
"activePlaceholder": "",
|
||||
"activeSessionHint": "",
|
||||
"agentMessages": "",
|
||||
"arguments": "",
|
||||
"collapseChat": "",
|
||||
"donePlaceholder": "",
|
||||
"doneSessionHint": "",
|
||||
"emptyAgentOutput": "",
|
||||
"entryCount_one": "",
|
||||
"entryCount_other": "",
|
||||
|
||||
@@ -8562,12 +8562,10 @@
|
||||
},
|
||||
"taskChat": {
|
||||
"activePlaceholder": "",
|
||||
"activeSessionHint": "",
|
||||
"agentMessages": "",
|
||||
"arguments": "",
|
||||
"collapseChat": "",
|
||||
"donePlaceholder": "",
|
||||
"doneSessionHint": "",
|
||||
"emptyAgentOutput": "",
|
||||
"entryCount_one": "",
|
||||
"entryCount_other": "",
|
||||
|
||||
2
packages/i18n/src/resources.d.ts
vendored
2
packages/i18n/src/resources.d.ts
vendored
@@ -7104,12 +7104,10 @@ export default interface Resources {
|
||||
},
|
||||
"taskChat": {
|
||||
"activePlaceholder": "Steer the currently executing agent",
|
||||
"activeSessionHint": "Message the active agent session. Guidance is delivered to the running session in real time.",
|
||||
"agentMessages": "{{label}} messages",
|
||||
"arguments": "Arguments",
|
||||
"collapseChat": "Collapse chat",
|
||||
"donePlaceholder": "Start a refinement task for this completed task",
|
||||
"doneSessionHint": "Send a message to start a refinement task for this completed task.",
|
||||
"emptyAgentOutput": "No agent output yet. Live messages from Planner, Executor, Reviewer, and Merger agents will appear here.",
|
||||
"entryCount_one": "{{count}} entry",
|
||||
"entryCount_other": "{{count}} entries",
|
||||
|
||||
Reference in New Issue
Block a user