Skip to content
Merged
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
19 changes: 18 additions & 1 deletion apps/my-wordpress/my-wordpress.os.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,11 @@ export default defineApp< AppState, AppData >( 'my-wordpress', {
cast.description = from.description;
cast.vibes = from.vibes;
cast.instructions = from.instructions;
// The brief IS the system prompt field, so the copied
// instructions land there too: Describe shows what was
// copied and lets it be edited, instead of an empty box
// over a prompt that only the summary card admits to.
cast.brief = from.instructions;
cast.role = from.role;
cast.abilities = [ ...from.abilities ];
cast.triggers = from.triggers.map( ( t ) => ( {
Expand All @@ -270,7 +275,19 @@ export default defineApp< AppState, AppData >( 'my-wordpress', {
state.briefError = '';
},
'agent-step': ( state, args ) => {
state.wstep = Math.max( 0, Math.min( 4, Number( args.step ) ) ) as AppState[ 'wstep' ];
const next = Math.max( 0, Math.min( 4, Number( args.step ) ) ) as AppState[ 'wstep' ];
// Leaving Describe by any door — Continue, or a jump from
// the trail — takes the brief with it: what the textarea
// labelled "system prompt" says is what the agent gets.
// Drafting writes its rewrite back into the brief, so this
// never undoes a draft; it only carries the words along.
if ( state.wstep === 0 && next !== 0 && state.cast ) {
state.cast = {
...state.cast,
instructions: String( state.cast.brief ?? '' ).trim(),
};
}
state.wstep = next;
state.agentNotice = '';
},
'agent-pane': ( state, args ) => {
Expand Down
24 changes: 10 additions & 14 deletions apps/my-wordpress/parts/agents-wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
* Describe (the starters and the AI draft), Meet (the face picker,
* the name, the voice), Powers (role + abilities), Summon (the
* trigger doors), Launch (the summary card and the create). Every
* earlier step stays reachable by clicking its number in the trail.
* step is reachable by clicking it in the trail, in either direction;
* only Launch's create insists on a name, and bounces to Meet without
* one.
* `renderAgents()` at the bottom is the section's view switch — the
* one entry the app's `renderBody()` calls.
*
Expand Down Expand Up @@ -65,9 +67,9 @@ function agentsWizard( ctx: Ctx, payload: AgentsPayload ): TemplateResult {
title=${ label }
?done=${ i < step }
?current=${ i === step }
?interactive=${ i < step }
?interactive=${ i !== step }
@os-step-click=${ () => {
if ( i < step ) {
if ( i !== step ) {
ctx.local( 'agent-step', { step: i } );
}
} }
Expand Down Expand Up @@ -105,12 +107,9 @@ function agentsCancelButton( ctx: Ctx, cast: CastDraft ): TemplateResult {

/** Step 0 — the door, then the brief. */
function agentsDescribeStep( ctx: Ctx, payload: AgentsPayload, cast: CastDraft ): TemplateResult {
// Their words are already a first draft of the instructions.
const seedFromBrief = (): void => {
if ( cast.instructions === '' && cast.brief.trim() !== '' ) {
cast.instructions = cast.brief.trim();
}
};
// The brief becomes the instructions on the way out: the
// `agent-step` local does it, so the trail's jumps carry the words
// along exactly as Continue does.
const draftWithAi = (): void => {
if ( cast.brief.trim() === '' ) {
ctx.local( 'agent-brief-error', {
Expand Down Expand Up @@ -199,10 +198,7 @@ function agentsDescribeStep( ctx: Ctx, payload: AgentsPayload, cast: CastDraft )
<os-button
variant=${ payload.aiReady ? 'ghost' : 'primary' }
?disabled=${ cast.drafting }
@click=${ () => {
seedFromBrief();
ctx.local( 'agent-step', { step: 1 } );
} }
@click=${ () => ctx.local( 'agent-step', { step: 1 } ) }
>
${ payload.aiReady ? __( 'I will fill it in myself' ) : __( 'Continue' ) }
</os-button>
Expand Down Expand Up @@ -473,7 +469,7 @@ function agentsLaunchStep( ctx: Ctx, payload: AgentsPayload, cast: CastDraft ):
${ cast.instructions === ''
? html`<p class="dm-agents__hint">
${ __(
'No instructions yet: the agent will improvise. You can add them any time in Define.',
'No instructions yet: the agent will improvise. Add them in Describe, or any time after in Define.',
) }
</p>`
: html`<p class="dm-agents__summary-instr">${ cast.instructions }</p>` }
Expand Down
6 changes: 6 additions & 0 deletions apps/my-wordpress/parts/agents.php
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,12 @@ function agent_draft_action( State $state ) {
if ( isset( $draft['abilities'] ) && is_array( $draft['abilities'] ) ) {
$cast['abilities'] = array_values( array_map( 'strval', $draft['abilities'] ) );
}
// The rewrite goes back into the brief: Describe's textarea is the
// system prompt, so walking back to it shows the drafted
// instructions, editable, rather than the sentence they grew from.
if ( '' !== (string) ( $cast['instructions'] ?? '' ) ) {
$cast['brief'] = (string) $cast['instructions'];
}
// Filled in, Meet is a review.
$state->set( 'cast', $cast )->set( 'wstep', 1 )->set( 'briefError', '' );
}
Expand Down
34 changes: 32 additions & 2 deletions apps/my-wordpress/parts/agents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,14 +213,37 @@ describe( 'agents helpers', () => {
data( { agents: agentsPayload() } ),
) as AppState;
expect( copied.wstep ).toBe( 1 );
const cast = copied.cast as { name: string; copiedFrom: string; faceSeed: number };
const cast = copied.cast as {
name: string;
copiedFrom: string;
faceSeed: number;
brief: string;
instructions: string;
};
expect( cast.name ).toContain( 'Indexer' );
expect( cast.copiedFrom ).toBe( 'Indexer' );
// A copy takes the work but not the face.
expect( cast.faceSeed ).not.toBe( 9 );
// The copied prompt is in the Describe textarea, not only in
// the summary card.
expect( cast.brief ).toBe( 'Index things.' );

const stepped = app.runLocal( 'agent-step', copied, { step: 3 }, data() ) as AppState;
expect( stepped.wstep ).toBe( 3 );

// Leaving Describe — by Continue or a trail jump — makes the
// brief the instructions; moving between later steps does not
// touch them.
const typed = { ...started, cast: { ...( started.cast as object ), brief: ' Watch my drafts. ' } };
const left = app.runLocal( 'agent-step', typed as AppState, { step: 3 }, data() ) as AppState;
expect( ( left.cast as { instructions: string } ).instructions ).toBe( 'Watch my drafts.' );
const later = app.runLocal(
'agent-step',
{ ...left, cast: { ...( left.cast as object ), brief: 'changed later' } } as AppState,
{ step: 4 },
data(),
) as AppState;
expect( ( later.cast as { instructions: string } ).instructions ).toBe( 'Watch my drafts.' );
const cancelled = app.runLocal( 'agent-cancel', stepped, {}, data() ) as AppState;
expect( cancelled.casting ).toBe( false );
expect( cancelled.cast ).toBeNull();
Expand Down Expand Up @@ -308,7 +331,14 @@ describe( 'agents view', () => {
agentsPayload(),
);
expect( root.textContent ).toContain( 'New agent' );
expect( root.querySelectorAll( 'os-step' ) ).toHaveLength( 5 );
const steps = root.querySelectorAll( 'os-step' );
expect( steps ).toHaveLength( 5 );
// Every step but the current one is a jump target, forward too.
expect( steps[ 0 ].hasAttribute( 'interactive' ) ).toBe( false );
expect( steps[ 0 ].hasAttribute( 'current' ) ).toBe( true );
expect( Array.from( steps ).slice( 1 ).every( ( s ) => s.hasAttribute( 'interactive' ) ) ).toBe(
true,
);
expect( root.textContent ).toContain( 'Start from someone' );
expect( root.textContent ).toContain( 'Draft it for me' );
expect( root.textContent ).toContain( 'I will fill it in myself' );
Expand Down
2 changes: 1 addition & 1 deletion docs/components-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ you relabel the host, e.g. Maximize ⇄ Restore.
| --- | --- | --- | --- |
| `<os-tabs>` / `<os-tab>` / `<os-tabpanel>` | `OsTabs`, `OsTab`, `OsTabPanel` | `os-tabs/os-tabs.ts` | Tab strip with associated panels, for a tab group **inside** content. A window's own top-level tabs belong in the window chrome instead — see `Window.setTabs()` in [`javascript-reference.md`](javascript-reference.md). |
| `<os-tab-chip>` | `OsTabChip` | `os-tab-chip/os-tab-chip.ts` | Single chip tab (e.g. window tabs). |
| `<os-steps>` / `<os-step>` | `OsSteps`, `OsStep` | `os-steps/os-steps.ts` | Numbered steps, stacked or as a horizontal trail. `current` marks where the reader is, `interactive` makes a step a jump target. |
| `<os-steps>` / `<os-step>` | `OsSteps`, `OsStep` | `os-steps/os-steps.ts` | Numbered steps, stacked or as a horizontal trail. `current` marks where the reader is, `interactive` makes a step a jump target with a hover state. |
| `<os-crumb-chain>` | `OsCrumbChain` | `os-crumb-chain/os-crumb-chain.ts` | Breadcrumb trail with chevron separators. |

## Color & theming
Expand Down
22 changes: 22 additions & 0 deletions src/ui/components/os-steps/os-steps.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,31 @@ export const stepStyles = css`
color: var( --os-ui-fg, #1d2327 );
font-weight: 600;
}
/*
* A jump target says so before it is pressed. The title steps up
* to full contrast, exactly as an <os-tabs> tab does on hover, and
* an outlined chip (a trail that set --os-ui-step-chip-border)
* takes the same ink; a filled chip has no border to recolour and
* keeps its fill, so this is safe on the default blue too. Inside
* the shell the pointer never shows: the cursor policy in
* desktop.css sets the host back to the arrow, by design, which
* is why the hover state is what says "this one can be clicked".
*/
:host( [ interactive ] ) {
cursor: pointer;
}
:host( [ interactive ] ) .os-step__title {
transition: color 120ms ease;
}
:host( [ interactive ] )::before {
transition: border-color 120ms ease;
}
:host( [ interactive ]:hover ) .os-step__title {
color: var( --os-ui-step-title-hover-color, var( --os-ui-fg, #1d2327 ) );
}
:host( [ interactive ]:hover )::before {
border-color: var( --os-ui-step-title-hover-color, var( --os-ui-fg, #1d2327 ) );
}
:host( [ interactive ]:focus-visible ) {
outline: var( --os-ui-focus-ring, 2px solid #2271b1 );
outline-offset: 2px;
Expand Down
8 changes: 7 additions & 1 deletion src/ui/components/os-steps/os-steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export class OsStep extends Component {
name: 'interactive',
type: 'boolean',
description:
'Makes the step a jump target: focusable, activated by click or Enter/Space, and emits os-step-click.',
'Makes the step a jump target: focusable, activated by click or Enter/Space, and emits os-step-click. Lifts the title to full contrast on hover.',
},
],
events: [
Expand All @@ -173,6 +173,12 @@ export class OsStep extends Component {
default: 'var(--os-ui-fg-muted)',
},
{ name: '--os-ui-step-chip-font-size', default: '13px' },
{
name: '--os-ui-step-title-hover-color',
default: 'var(--os-ui-fg)',
description:
'Ink an interactive step takes on hover, on its title and on an outlined chip.',
},
],
example: html`
<os-steps>
Expand Down
1 change: 1 addition & 0 deletions tests/phpunit/tests/myWordPressAppAgents.php
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ static function () {
$this->assertSame( 'Draft Rider', $cast['name'] );
$this->assertSame( 'swift', $cast['vibes'] );
$this->assertSame( 'Do the rounds.', $cast['instructions'] );
$this->assertSame( 'Do the rounds.', $cast['brief'], 'The rewrite lands in the brief, so Describe shows and edits the drafted prompt.' );
$this->assertSame( 'author', $cast['role'], 'A role the site does not allow is dropped, keeping the cast\'s.' );
$this->assertSame( array(), $cast['abilities'], 'Unknown abilities are filtered out.' );
$this->assertFalse( $cast['drafting'], 'The in-flight flag is lowered in the returned state.' );
Expand Down
Loading