Skip to content

Commit a6b2cff

Browse files
Riad Benguellaclaude
authored andcommitted
Show save confirmation and character-limit indicator in Studio Code instructions settings
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 509fa2c commit a6b2cff

5 files changed

Lines changed: 74 additions & 15 deletions

File tree

‎apps/cli/ai/system-prompt.ts‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { GLOBAL_INSTRUCTIONS_MAX_LENGTH } from '@studio/common/ai/global-instructions';
12
import {
23
getStudioPresentationRulesPrompt,
34
getStudioWidgetPromptManifest,
@@ -27,10 +28,6 @@ export interface BuildSystemPromptOptions {
2728
userInstructions?: string;
2829
}
2930

30-
// Keep the always-injected instructions small enough that they can't crowd out
31-
// the rest of the system prompt.
32-
const USER_INSTRUCTIONS_MAX_LENGTH = 16_000;
33-
3431
export function buildSystemPrompt( options?: BuildSystemPromptOptions ): string {
3532
const remoteSessionAddendum = options?.remoteSession ? `\n\n${ REMOTE_SESSION_GUIDANCE }` : '';
3633
const userInstructionsSection = buildUserInstructionsSection( options?.userInstructions );
@@ -59,10 +56,10 @@ function buildUserInstructionsSection( userInstructions?: string ): string {
5956
return '';
6057
}
6158
const instructions =
62-
userInstructions.length > USER_INSTRUCTIONS_MAX_LENGTH
59+
userInstructions.length > GLOBAL_INSTRUCTIONS_MAX_LENGTH
6360
? `${ userInstructions.slice(
6461
0,
65-
USER_INSTRUCTIONS_MAX_LENGTH
62+
GLOBAL_INSTRUCTIONS_MAX_LENGTH
6663
) }\n\n[Note: the global instructions file exceeds the size limit and was truncated here. Let the user know they should shorten it in Studio settings.]`
6764
: userInstructions;
6865
return `

‎apps/studio/src/modules/user-settings/components/studio-code-tab.tsx‎

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { GLOBAL_INSTRUCTIONS_MAX_LENGTH } from '@studio/common/ai/global-instructions';
12
import { getErrorMessage } from '@studio/common/lib/error-formatting';
23
import { TextareaControl } from '@wordpress/components';
34
import { useI18n } from '@wordpress/react-i18n';
@@ -10,6 +11,7 @@ export function StudioCodeTab() {
1011
const [ content, setContent ] = useState< string | null >( null );
1112
const [ savedContent, setSavedContent ] = useState( '' );
1213
const [ isSaving, setIsSaving ] = useState( false );
14+
const [ justSaved, setJustSaved ] = useState( false );
1315
const [ error, setError ] = useState< string | null >( null );
1416

1517
useEffect( () => {
@@ -32,6 +34,14 @@ export function StudioCodeTab() {
3234
};
3335
}, [] );
3436

37+
useEffect( () => {
38+
if ( ! justSaved ) {
39+
return;
40+
}
41+
const timer = setTimeout( () => setJustSaved( false ), 2500 );
42+
return () => clearTimeout( timer );
43+
}, [ justSaved ] );
44+
3545
const handleSave = async () => {
3646
if ( content === null ) {
3747
return;
@@ -41,13 +51,22 @@ export function StudioCodeTab() {
4151
try {
4252
await getIpcApi().saveGlobalAgentInstructions( content );
4353
setSavedContent( content );
54+
setJustSaved( true );
4455
} catch ( err ) {
4556
setError( getErrorMessage( err ) ?? String( err ) );
4657
} finally {
4758
setIsSaving( false );
4859
}
4960
};
5061

62+
const isDirty = content !== null && content !== savedContent;
63+
const showCounter = content !== null && content.length >= GLOBAL_INSTRUCTIONS_MAX_LENGTH * 0.8;
64+
const buttonLabel = isSaving
65+
? __( 'Saving…' )
66+
: justSaved && ! isDirty
67+
? __( 'Saved' )
68+
: __( 'Save instructions' );
69+
5170
return (
5271
<div className="flex flex-col gap-4 pb-2">
5372
{ error && <div className="text-sm text-frame-error">{ error }</div> }
@@ -59,19 +78,25 @@ export function StudioCodeTab() {
5978
'Global instructions for the Studio Code agent. They are included in every new conversation, across all sites.'
6079
) }
6180
rows={ 12 }
81+
maxLength={ GLOBAL_INSTRUCTIONS_MAX_LENGTH }
6282
value={ content ?? '' }
6383
onChange={ setContent }
6484
disabled={ content === null }
6585
placeholder={ __( 'e.g. Always answer in French. My sites are for restaurants.' ) }
6686
/>
6787

68-
<div className="flex justify-end">
88+
<div className="flex items-center justify-end gap-3">
89+
{ showCounter && (
90+
<span className="text-xs text-frame-text-secondary">
91+
{ `${ content.length.toLocaleString() } / ${ GLOBAL_INSTRUCTIONS_MAX_LENGTH.toLocaleString() }` }
92+
</span>
93+
) }
6994
<Button
7095
variant="primary"
7196
onClick={ handleSave }
72-
disabled={ content === null || isSaving || content === savedContent }
97+
disabled={ content === null || isSaving || ! isDirty }
7398
>
74-
{ isSaving ? __( 'Saving…' ) : __( 'Save instructions' ) }
99+
{ buttonLabel }
75100
</Button>
76101
</div>
77102
</div>

‎apps/ui/src/components/settings-view/studio-code-panel.tsx‎

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { GLOBAL_INSTRUCTIONS_MAX_LENGTH } from '@studio/common/ai/global-instructions';
12
import { DataForm } from '@wordpress/dataviews';
23
import { __ } from '@wordpress/i18n';
34
import { Button } from '@wordpress/ui';
4-
import { useState } from 'react';
5+
import { useEffect, useState } from 'react';
56
import {
67
useAgentInstructions,
78
useSaveAgentInstructions,
@@ -36,20 +37,36 @@ export function StudioCodePanel() {
3637
const { data: saved } = useAgentInstructions();
3738
const saveInstructions = useSaveAgentInstructions();
3839
const [ edits, setEdits ] = useState< string | null >( null );
40+
const [ justSaved, setJustSaved ] = useState( false );
41+
42+
useEffect( () => {
43+
if ( ! justSaved ) {
44+
return;
45+
}
46+
const timer = setTimeout( () => setJustSaved( false ), 2500 );
47+
return () => clearTimeout( timer );
48+
}, [ justSaved ] );
3949

4050
if ( saved === undefined ) {
4151
return <div className={ styles.state }>{ __( 'Loading…' ) }</div>;
4252
}
4353

4454
const content = edits ?? saved;
45-
const canSubmit = content !== saved && ! saveInstructions.isPending;
55+
const isDirty = content !== saved;
56+
const canSubmit = isDirty && ! saveInstructions.isPending;
57+
const showCounter = content.length >= GLOBAL_INSTRUCTIONS_MAX_LENGTH * 0.8;
4658

4759
const handleSubmit = ( event: FormEvent ) => {
4860
event.preventDefault();
4961
if ( ! canSubmit ) {
5062
return;
5163
}
52-
saveInstructions.mutate( content, { onSuccess: () => setEdits( null ) } );
64+
saveInstructions.mutate( content, {
65+
onSuccess: () => {
66+
setEdits( null );
67+
setJustSaved( true );
68+
},
69+
} );
5370
};
5471

5572
return (
@@ -58,14 +75,23 @@ export function StudioCodePanel() {
5875
data={ { content } }
5976
fields={ FIELDS }
6077
form={ FORM }
61-
onChange={ ( update ) => setEdits( ( update.content as string ) ?? '' ) }
78+
onChange={ ( update ) =>
79+
setEdits(
80+
( ( update.content as string ) ?? '' ).slice( 0, GLOBAL_INSTRUCTIONS_MAX_LENGTH )
81+
)
82+
}
6283
/>
6384
{ saveInstructions.isError && (
6485
<p className={ styles.instructionsError }>
6586
{ __( 'Saving the instructions failed. Please try again.' ) }
6687
</p>
6788
) }
6889
<div className={ styles.actions }>
90+
{ showCounter && (
91+
<span className={ styles.instructionsCounter }>
92+
{ `${ content.length.toLocaleString() } / ${ GLOBAL_INSTRUCTIONS_MAX_LENGTH.toLocaleString() }` }
93+
</span>
94+
) }
6995
<Button
7096
type="submit"
7197
variant="solid"
@@ -74,7 +100,7 @@ export function StudioCodePanel() {
74100
loading={ saveInstructions.isPending }
75101
loadingAnnouncement={ __( 'Saving instructions' ) }
76102
>
77-
{ __( 'Save instructions' ) }
103+
{ justSaved && ! isDirty ? __( 'Saved' ) : __( 'Save instructions' ) }
78104
</Button>
79105
</div>
80106
</form>

‎apps/ui/src/components/settings-view/style.module.css‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,14 @@
129129

130130
.actions {
131131
display: flex;
132+
align-items: center;
132133
justify-content: flex-end;
133-
gap: var(--wpds-dimension-padding-sm);
134+
gap: var(--wpds-dimension-padding-md);
135+
}
136+
137+
.instructionsCounter {
138+
font-size: var(--wpds-typography-font-size-sm);
139+
color: var(--wpds-color-fg-content-neutral-weak);
134140
}
135141

136142
.instructionsError {

‎packages/common/ai/global-instructions.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import path from 'path';
33
import { isErrnoException } from '../lib/is-errno-exception';
44
import { getGlobalInstructionsPath } from '../lib/well-known-paths';
55

6+
// Keep the always-injected instructions small enough that they can't crowd out
7+
// the rest of the system prompt. The prompt builder truncates anything longer;
8+
// the settings UIs cap their textareas at the same length.
9+
export const GLOBAL_INSTRUCTIONS_MAX_LENGTH = 16_000;
10+
611
/**
712
* Read the user's global agent instructions (`~/.studio/knowledge/instructions.md`)
813
* for injection into the system prompt. Returns `undefined` when the file is

0 commit comments

Comments
 (0)