Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion backend/scripts/seed-skills/theming.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,25 @@ Keep experimental overlays bounded and cheap. Full-viewport animated gradients a

Read source first, then save your edits under `/data/platform/frontend/src/`. A file watcher runs `vite build` into the served `dist/` on every source change (debounced, atomic swap) — there is NO manual rebuild step and NO restart. Just reload the page to see the change. Batch all edits so the watcher rebuilds once instead of on every save. For CSS-only changes, prefer `theme.css` above (hot-reloaded, no build at all). If the shell breaks, direct the partner to `/recover` → "Restore platform" (see `recovery.md`).

After finishing a burst of shell edits, wait for the watcher build to land before POSTing `{"type":"shell_apply_now"}` to `/api/notify` with the same authenticated call shape as `notify_theme.sh`. The watcher builds within a few seconds of the last save; the rebuild events ride the shell's own system event stream (not the chat), so you won't see them here — give the build a few seconds, then confirm the build actually carried your change: `grep` the served bundle (`/data/platform/frontend/dist/assets/index-*.js`) for a distinctive string you just added. A fresh `dist/` mtime alone can mislead (an incremental/cached build can rewrite the file without your change), which is how a "rebuilt" shell can still serve the old code — grep for the change, don't trust the timestamp.
After finishing a burst of shell edits, wait for the watcher build to land, then
request the apply. This endpoint deliberately returns an empty `204` success, so
discard its body — do **not** pipe it to a JSON parser:

```bash
curl -fsS -o /dev/null -X POST "$API_BASE_URL/api/notify" \
-H "Authorization: Bearer $AGENT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"type":"shell_apply_now"}'
```

The watcher builds within a few seconds of the last save; the rebuild events
ride the shell's own system event stream (not the chat), so you won't see them
here — give the build a few seconds, then confirm the build actually carried
your change: `grep` the served bundle
(`/data/platform/frontend/dist/assets/index-*.js`) for a distinctive string you
just added. A fresh `dist/` mtime alone can mislead (an incremental/cached build
can rewrite the file without your change), which is how a "rebuilt" shell can
still serve the old code — grep for the change, don't trust the timestamp.

After a git/platform update, not a normal save, the watcher sees no edit event; kick it explicitly by touching a changed file under `/data/platform/frontend/src`, then restart if prompted. The updater does not auto-detect frontend changes by design, so run the step explicitly after frontend-touching platform updates.

Expand Down
16 changes: 2 additions & 14 deletions frontend/src/components/ChatView/ActivityLineHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ const ActivityLineHeader = forwardRef(function ActivityLineHeader({
text,
displayState,
iconKind,
exitCode = null,
interactive = false,
open = false,
ariaLabel,
Expand Down Expand Up @@ -110,18 +109,10 @@ const ActivityLineHeader = forwardRef(function ActivityLineHeader({
>
<span
className="chat__activity-icon"
data-activity-kind={displayState === 'error' ? undefined : iconKind}
data-activity-kind={iconKind}
aria-hidden="true"
>
{displayState === 'error' ? (
<svg viewBox="0 0 16 16" width="13" height="13" fill="none"
stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"
strokeLinejoin="round">
<path d="M8 2 15 14H1z" /><path d="M8 6v4" /><path d="M8 12h.01" />
</svg>
) : (
<ActivityTypeIcon kind={iconKind} />
)}
<ActivityTypeIcon kind={iconKind} />
</span>
<span className="chat__activity-label">
<span className="chat__activity-label-text">{text}</span>
Expand All @@ -134,9 +125,6 @@ const ActivityLineHeader = forwardRef(function ActivityLineHeader({
// owns it so it reads at a glance without expanding the line.
<span className="chat__activity-count">{count}</span>
)}
{displayState === 'error' && exitCode != null && (
<span className="chat__activity-chip">exit {exitCode}</span>
)}
</Header>
)
})
Expand Down
47 changes: 15 additions & 32 deletions frontend/src/components/ChatView/ActivityStretch.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
activityCollapsedLabel,
thoughtDurationLabel,
} from './groupBlocks.js'
import { toolBlockExitCode } from './toolResultFormat.js'
import { toolActivityIcon, effectiveToolName } from './toolActivityLabel.js'
import { thinkingContentForDisplay } from './streamReducers.js'
import { assistantBlockKey } from './streamPromotion.js'
Expand All @@ -24,11 +23,11 @@ import { useDisclosureState } from './disclosureState.js'
// quiet ~32px line instead of alternating rows — the answer keeps the screen.
// A lone thought/tool renders as its own disclosure (see SingleActivity below):
// wrapping one row in an identical parent adds hierarchy without information.
// Collapsed, the borderless dim header carries live
// status (a periodic shimmer over the label — bare "Thinking", or the muted
// type glyph + progressive activities while tools run) and a FAILED step's
// danger triangle + exit chip, all readable
// WITHOUT expanding. Expanded, it renders the chronological timeline: mixed
// Collapsed, the borderless dim header carries only live status (a periodic
// shimmer over the label — bare "Thinking", or the muted type glyph +
// progressive activities while tools run). A command exit is diagnostic detail,
// not a verdict on the turn, so it stays inside expansion. Expanded, the line
// renders the chronological timeline: mixed
// thinking entries and tools become independently collapsed child rows, so
// opening the overview never spills a full reasoning trace or tool output into
// the transcript. A thought keeps this same child component as tools arrive,
Expand Down Expand Up @@ -284,39 +283,27 @@ function GroupedActivityStretch({
failedHelpers > 0 ? `${failedHelpers} failed` : null,
].filter(Boolean).join(' · ')
: null
// Deriving the state parses each tool's output for its exit code, so memoize
// on a cheap signature (see activityMemoSig for the exact staleness contract:
// head+tail output slices catch an equal-length exit-code flip; thinking
// content never busts the memo on typewriter frames).
// The overview depends only on tool identity/status. Command output and exact
// failures stay with ToolBlock after expansion, so typewriter/output frames do
// not churn every collapsed activity summary above the live turn.
const sig = activityMemoSig(entries, { liveThinkingTail })

const meta = useMemo(() => {
const tools = entries
.filter(e => e?.item?.type === 'tool')
.map(e => e.item)
const state = activityStreamState(tools, { liveThinkingTail })
// The collapsed exit chip shows the most-recent failed tool's code (the same
// "exit N" the ToolBlock header carries), so a failed step is legible without
// opening. Only computed once the stretch has settled to 'error'.
let exitCode = null
if (state === 'error') {
for (const t of tools) {
const code = toolBlockExitCode(t)
if (code != null && code !== 0) exitCode = code
}
}
return {
state,
exitCode,
toolCount: Number.isInteger(summaryToolCount)
? summaryToolCount
: tools.length,
thinkingOnly: tools.length === 0,
}
}, [sig, summaryToolCount]) // eslint-disable-line react-hooks/exhaustive-deps

const { state, exitCode, toolCount, thinkingOnly } = meta
// The one presentation authority for icon, chip, and state class: a live
const { state, toolCount, thinkingOnly } = meta
// The one presentation authority for icon and state class: a live
// stretch reads in-progress for its whole life — the tool→tool gap included —
// so icon and tense can never contradict (see activityDisplayState). Applied
// OUTSIDE the memo because `live` is not part of the signature.
Expand All @@ -333,17 +320,14 @@ function GroupedActivityStretch({
// header comment). While collapsed, the header status carries liveness.
const open = userOpen

// The step count and failure detail ride in the accessible name only (the
// visible line stays a calm activity summary); the one-second clock is not in
// an aria-live region, so a screen reader is not re-announced every tick.
// The step count rides in the accessible name. Command diagnostics do not:
// screen-reader users get the same calm overview and can inspect the same
// expanded child rows. The one-second clock is not in an aria-live region,
// so it is not re-announced every tick.
const stepNote = toolCount > 0
? ` (${toolCount} ${toolCount === 1 ? 'step' : 'steps'})`
: ''
const stateNote = displayState === 'error'
? `, a step failed${exitCode != null ? ` with exit ${exitCode}` : ''}`
: displayState === 'running'
? ', in progress'
: ''
const stateNote = displayState === 'running' ? ', in progress' : ''
const iconKind = thinkingOnly ? 'reasoning' : leadToolIcon
const timelineEntries = detailRef ? detailEntries : entries

Expand All @@ -357,7 +341,6 @@ function GroupedActivityStretch({
text={text}
displayState={displayState}
iconKind={iconKind}
exitCode={exitCode}
interactive
open={open}
ariaLabel={`${text}${stepNote}${stateNote}`}
Expand Down
76 changes: 15 additions & 61 deletions frontend/src/components/ChatView/ChatView.css
Original file line number Diff line number Diff line change
Expand Up @@ -362,36 +362,6 @@
}
.chat__ts--visible { opacity: 1; }

/* Holding a mobile message copies immediately. The only visible UI is this
brief confirmation, kept above the composer so it never shifts content. */
.chat__copy-toast {
position: absolute;
left: 50%;
bottom: calc(var(--composer-h, 72px) + 18px);
z-index: 230;
transform: translateX(-50%);
max-width: calc(100% - 32px);
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: 999px;
background: color-mix(in srgb, var(--surface) 92%, transparent);
backdrop-filter: blur(16px) saturate(140%);
color: var(--text);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.22);
font-size: 12px;
font-weight: 600;
white-space: nowrap;
animation: chat-copy-toast-in 0.18s var(--ease-out-soft, ease-out);
}
.chat__copy-toast--success svg { color: var(--accent); }

@keyframes chat-copy-toast-in {
from { opacity: 0; transform: translate(-50%, 6px); }
}

/* ── Evolving chat summary ─────────────────────────── */
.chat-summary__overlay {
position: absolute;
Expand Down Expand Up @@ -810,8 +780,9 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
/* The type icon leads this label. The exit chip hugs the text instead of
drifting to the far edge of a wide activity row. */
/* The type icon leads this label. When this is a child row already revealed
by an expanded activity, its exit chip hugs the text instead of drifting
to the far edge. */
flex: 0 1 auto;
}

Expand Down Expand Up @@ -1155,17 +1126,17 @@
border: 1px solid color-mix(in srgb, var(--danger) 30%, var(--border-light)); /* CONTRACT: danger-tinted hairline */
}

/* The same exit chip on the COLLAPSED tool header, so a failed command shows
its nonzero exit without expanding (a tool never carries an 'error' status —
the exit code is the only failure signal). */
/* Inside an expanded multi-step activity, a failed child row keeps the same
compact exit chip. A direct top-level compact row waits until its own output
is expanded, where `.chat__tool-exit` above renders the exact code. */
.chat__tool-exit--head {
flex-shrink: 0;
align-self: auto;
}

/* A failed tool's header name tints toward danger so the whole row reads as a
failure at a glance, not just the chip. */
.chat__tool--failed .chat__tool-name {
/* A failed child inside an already-expanded activity tints toward danger.
Direct compact transcript rows stay neutral until explicitly opened. */
.chat__tool:not(.chat__tool--compact).chat__tool--failed .chat__tool-name {
color: color-mix(in srgb, var(--danger) 70%, var(--text)); /* CONTRACT: danger-leaning tool name on a failed row */
}

Expand Down Expand Up @@ -1217,8 +1188,9 @@
borderless dim line, so a build turn's pre-prose burst stops burying the
answer. A lone thought or tool uses its own disclosure directly: hierarchy
is added only when there is something to group. Structure is neutral; the
only hue is --danger for a failed step and --accent for keyboard focus.
Collapsed by default; status remains readable without expanding. See
only accent hue is the keyboard focus ring. Command-level diagnostics stay
inside expansion because a nonzero shell exit is not a turn-level verdict.
Collapsed by default; liveness remains readable without expanding. See
ActivityStretch.jsx + groupBlocks.js. */
.chat__activity {
margin: 0;
Expand Down Expand Up @@ -1268,15 +1240,9 @@
color: var(--muted); /* CONTRACT: low-contrast status glyph */
}

.chat__activity--error .chat__activity-icon {
color: var(--danger); /* CONTRACT: vivid danger — a step in the stretch failed */
}

.chat__activity-label {
/* No stretch: the label and its trailing chip group left as one compact
unit (the ChatGPT idiom) rather than spreading across the row. The header
button is still full-width — only the visual grouping is left-aligned.
Long labels truncate before the danger chip clips. */
/* No stretch: the calm label stays left-aligned rather than spreading across
the row. The header button remains full-width. */
flex: 0 1 auto;
min-width: 0;
overflow: hidden;
Expand Down Expand Up @@ -1342,17 +1308,6 @@
100% { -webkit-mask-position: -50% 0; mask-position: -50% 0; }
}

.chat__activity-chip {
flex-shrink: 0;
padding: 0 6px;
border-radius: 8px;
font-family: var(--mono); /* CONTRACT: monospace exit-code chip, matches the tool block */
font-size: 11px;
color: var(--danger); /* CONTRACT: vivid danger text — a nonzero exit failed */
background: color-mix(in srgb, var(--danger) 12%, transparent); /* CONTRACT: faint danger tint on the opaque message-list fill */
border: 1px solid color-mix(in srgb, var(--danger) 30%, var(--border-light)); /* CONTRACT: danger-tinted hairline */
}

/* Rotating disclosure chevron for the MARKER card (the activity line dropped
its chevron — the quiet line itself is the affordance), matching the
queued-tray idiom: the SVG points down when open and rotates to point right
Expand Down Expand Up @@ -2645,8 +2600,7 @@
}

@media (prefers-reduced-motion: reduce) {
.chat__build-rail,
.chat__copy-toast { animation: none; }
.chat__build-rail { animation: none; }
.chat__send,
.chat__steer,
.chat__stop { animation: none; }
Expand Down
Loading