feat(shell): yellow active session, growing composer, phase title, refined activity log, arrow panel toggle

Fix six GUI defects, verified live against the real stack:

- NavSessions: mark the in-progress (active) session in yellow (warning
  token) — both its status dot and its row background.
- SteerInput: the composer is now an auto-growing textarea that wraps and
  grows vertically (caps at 160px, then scrolls); Enter sends, Shift+Enter
  inserts a newline.
- WorkflowBar: show a synthetic title of the current phase under the 8 dots
  from static EN/IT strings (no LLM); English is displayed to match the chrome.
- CentralStatus: reformat the 5-line system-message tail as a structured,
  monospace list with per-line markers and an emphasized last line.
- AppShell + CentralStatus: move the left Model-activity panel toggle off the
  working spinner (now a pure status indicator) onto a dedicated arrow button
  (→ opens, ← closes).
- gate: rename the phase-confirm button "Conferma e prosegui" → "Salva e
  procedi" (builders.js + its L1 test).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 14:32:01 +02:00
co-authored by Claude Opus 4.8
parent 1f9818c50f
commit d6b4729ddf
8 changed files with 132 additions and 59 deletions
+40 -27
View File
@@ -2,27 +2,22 @@ import { useEffect, useReducer, useState } from "react";
import { useSessionStore } from "../store/sessionStore";
import { WorkingSpinner } from "./WorkingSpinner";
/** Last few non-blank lines of a string — a compact multi-line "what the model is
* doing now" tail (mirrors ModelActivityPanel's tail-line logic, at a smaller size). */
function tailLines(s: string, n = 5): string {
const lines = s
/** Last few non-blank lines of a string — a compact "what the model is doing now"
* tail (mirrors ModelActivityPanel's tail-line logic, at a smaller size). Returned
* as an array so each line renders as its own formatted log row. */
function tailLines(s: string, n = 5): string[] {
return s
.split("\n")
.map((l) => l.trimEnd())
.filter((l) => l.trim() !== "");
return lines.slice(-n).join("\n");
.filter((l) => l.trim() !== "")
.slice(-n);
}
/** The minimal central view: the user's last input/choice, a live "working" indicator
* (elapsed time + a short tail of the model stream) while the model is busy, and the
* gate's curated messages for the current step. The full verbose stream lives in the
* left Model-activity panel. */
export function CentralStatus({
working,
onOpenActivity,
}: {
working: boolean;
onOpenActivity?: () => void;
}) {
export function CentralStatus({ working }: { working: boolean }) {
const lastUserEntry = useSessionStore((s) => s.lastUserEntry);
const stepMessages = useSessionStore((s) => s.stepMessages);
const transcript = useSessionStore((s) => s.transcript);
@@ -45,7 +40,7 @@ export function CentralStatus({
if (!working && !lastUserEntry && stepMessages.length === 0) return null;
const elapsed = startedAt ? Math.max(0, Math.floor((Date.now() - startedAt) / 1000)) : 0;
const tail = tailLines(transcript.at(-1)?.text ?? "");
const tailRows = tailLines(transcript.at(-1)?.text ?? "");
return (
<div className="flex flex-col gap-3">
@@ -59,24 +54,42 @@ export function CentralStatus({
)}
{working && (
<div className="flex flex-col gap-1">
<div className="flex flex-col gap-1.5">
<div className="flex min-w-0 items-center gap-2 text-sm text-muted-foreground">
<button
type="button"
onClick={onOpenActivity}
aria-label="Show model activity"
title="Model activity"
className="grid size-5 shrink-0 place-items-center rounded text-primary transition-colors hover:bg-muted"
>
{/* Pure status indicator — opening the activity panel now lives on the
dedicated arrow toggle in the header, not on this spinner. */}
<span className="grid size-5 shrink-0 place-items-center text-primary">
<WorkingSpinner spinning className="size-3.5" />
</button>
</span>
<span className="shrink-0 tabular-nums">{elapsed}s</span>
<span className="sr-only">ThothII is working</span>
</div>
{tail && (
<p className="line-clamp-5 whitespace-pre-wrap break-words pl-7 text-sm text-muted-foreground/70">
{tail}
</p>
{tailRows.length > 0 && (
<ol className="ml-7 flex flex-col gap-0.5 overflow-hidden rounded-md border border-border/50 bg-muted/40 px-3 py-2">
{tailRows.map((line, i) => {
const isLast = i === tailRows.length - 1;
return (
<li
key={i}
className={[
"flex items-baseline gap-2 font-mono text-[0.72rem] leading-relaxed",
isLast ? "text-foreground/80" : "text-muted-foreground/55",
].join(" ")}
>
<span
aria-hidden
className={[
"shrink-0 select-none",
isLast ? "text-primary" : "text-muted-foreground/35",
].join(" ")}
>
›
</span>
<span className="min-w-0 flex-1 truncate">{line}</span>
</li>
);
})}
</ol>
)}
</div>
)}